hyf

Context-aware query service for Radroots
git clone https://radroots.dev/git/hyf.git
Log | Files | Refs | README | LICENSE

strict_fixture.mojo (43289B)


      1 """Strict bounded HTTP request framing and scripted exchanges for local test
      2 fixtures (ADR-0012 D29, ADR-0014 D33 FX01-FX05).
      3 
      4 This module owns the *test fixture* wire contract only. It deliberately does
      5 not change HYF's product HTTP/TLS policy or timeouts.
      6 
      7 Framing contract (FX03):
      8 
      9 * bounded byte-oriented accumulation before any UTF-8 decode
     10 * header cap 65,536 bytes and body cap 1,048,576 bytes (ADR-0010 D21)
     11 * lexical ``Content-Length``: nonempty ASCII digits only, with an explicit
     12   overflow guard; no sign, whitespace or non-digit
     13 * request line must be ``METHOD SP target SP HTTP/1.1|HTTP/1.0``
     14 * malformed header syntax (whitespace before ``:``, obs-fold, control bytes,
     15   non-token name bytes) is rejected rather than silently stripped
     16 * duplicate/conflicting framing and unsupported transfer encodings rejected
     17 * premature EOF and oversize rejected before unbounded accumulation
     18 * surplus bytes after a frame are retained for the next frame
     19 
     20 Exchange contract (FX01/FX02/FX04/FX05): explicit ordered scripts carrying the
     21 expected method/path/selected headers/body plus the scripted status/headers/
     22 body/delay and connection semantics. Request and connection counters are
     23 distinct and verified.
     24 """
     25 
     26 from std.collections import List
     27 from std.ffi import ErrNo, get_errno
     28 
     29 from flare.net import Timeout
     30 from flare.tcp import TcpListener
     31 from flare.tcp import TcpStream
     32 from flare.utils import usleep
     33 
     34 
     35 comptime STRICT_MAX_HEADER_BYTES = 65536
     36 comptime STRICT_MAX_BODY_BYTES = 1048576
     37 comptime STRICT_MAX_READY_BYTES = 256
     38 comptime STRICT_MAX_REPORT_BYTES = 2048
     39 comptime STRICT_COMPLETION_GRACE_MS = 100
     40 
     41 
     42 # ── JSON-safe escaping (FX04) ───────────────────────────────────────────────
     43 
     44 
     45 def _hex_digit(value: Int) -> UInt8:
     46     if value < 10:
     47         return UInt8(48 + value)
     48     return UInt8(97 + (value - 10))
     49 
     50 
     51 def json_escape(value: String) -> String:
     52     """Return ``value`` as a JSON string literal with full control escaping."""
     53     var out = List[UInt8]()
     54     out.append(UInt8(34))
     55     for byte in value.as_bytes():
     56         var b = Int(byte)
     57         if b == 34:
     58             out.append(UInt8(92))
     59             out.append(UInt8(34))
     60         elif b == 92:
     61             out.append(UInt8(92))
     62             out.append(UInt8(92))
     63         elif b == 8:
     64             out.append(UInt8(92))
     65             out.append(UInt8(98))
     66         elif b == 12:
     67             out.append(UInt8(92))
     68             out.append(UInt8(102))
     69         elif b == 10:
     70             out.append(UInt8(92))
     71             out.append(UInt8(110))
     72         elif b == 13:
     73             out.append(UInt8(92))
     74             out.append(UInt8(114))
     75         elif b == 9:
     76             out.append(UInt8(92))
     77             out.append(UInt8(116))
     78         elif b < 32 or b == 127:
     79             out.append(UInt8(92))
     80             out.append(UInt8(117))
     81             out.append(UInt8(48))
     82             out.append(UInt8(48))
     83             out.append(_hex_digit(b // 16))
     84             out.append(_hex_digit(b % 16))
     85         else:
     86             out.append(UInt8(b))
     87     out.append(UInt8(34))
     88     return String(unsafe_from_utf8=Span(ptr=out.unsafe_ptr(), length=len(out)))
     89 
     90 
     91 # ── Header access (FX03/FX04) ───────────────────────────────────────────────
     92 
     93 
     94 def header_values(headers_raw: String, name: String) -> List[String]:
     95     """Return every value for an exactly named header (case-insensitive name).
     96 
     97     The header name is compared byte-exactly after lowercasing; malformed
     98     spacing was already rejected by the framer, so no whitespace stripping of
     99     the name token occurs here.
    100     """
    101     var out = List[String]()
    102     var lines = headers_raw.split("\r\n")
    103     var wanted = name.lower()
    104     for i in range(1, len(lines)):
    105         var line = String(lines[i])
    106         var colon = line.find(":")
    107         if colon <= 0:
    108             continue
    109         if String(line[byte=0:colon]).lower() == wanted:
    110             out.append(String(line[byte = colon + 1 :].strip()))
    111     return out^
    112 
    113 
    114 def header_value(headers_raw: String, name: String) -> String:
    115     var values = header_values(headers_raw, name)
    116     if len(values) != 1:
    117         return ""
    118     return values[0]
    119 
    120 
    121 def bearer_token(headers_raw: String) -> String:
    122     var values = header_values(headers_raw, "authorization")
    123     if len(values) != 1:
    124         return ""
    125     if not values[0].startswith("Bearer "):
    126         return ""
    127     return String(values[0][byte=7:])
    128 
    129 
    130 def authorization_reason(headers_raw: String, required: Bool) -> String:
    131     """Validate the exact ``Authorization`` header, rejecting spoofs (FX04)."""
    132     if not required:
    133         return ""
    134     var values = header_values(headers_raw, "authorization")
    135     if len(values) == 0:
    136         return "auth_missing"
    137     if len(values) > 1:
    138         return "auth_duplicate"
    139     if not values[0].startswith("Bearer ") or values[0].byte_length() <= 7:
    140         return "auth_invalid"
    141     return ""
    142 
    143 
    144 # ── Framing ─────────────────────────────────────────────────────────────────
    145 
    146 
    147 @fieldwise_init
    148 struct FramedRequest(Movable):
    149     var ok: Bool
    150     var error: String
    151     var method: String
    152     var path: String
    153     var version: String
    154     var headers_raw: String
    155     var body: String
    156     var content_length: Int
    157     var keep_alive: Bool
    158     var total_bytes: Int
    159 
    160 
    161 def _token_byte(value: Int) -> Bool:
    162     if value >= 48 and value <= 57:
    163         return True
    164     if value >= 65 and value <= 90:
    165         return True
    166     if value >= 97 and value <= 122:
    167         return True
    168     # RFC 7230 token punctuation.
    169     return (
    170         value == 33
    171         or value == 35
    172         or value == 36
    173         or value == 37
    174         or value == 38
    175         or value == 39
    176         or value == 42
    177         or value == 43
    178         or value == 45
    179         or value == 46
    180         or value == 94
    181         or value == 95
    182         or value == 96
    183         or value == 124
    184         or value == 126
    185     )
    186 
    187 
    188 def _valid_method(method: String) -> Bool:
    189     if method.byte_length() == 0:
    190         return False
    191     for byte in method.as_bytes():
    192         var b = Int(byte)
    193         if b < 65 or b > 90:
    194             return False
    195     return True
    196 
    197 
    198 def _valid_version(version: String) -> Bool:
    199     return version == "HTTP/1.1" or version == "HTTP/1.0"
    200 
    201 
    202 def _valid_header_name(name: String) -> Bool:
    203     if name.byte_length() == 0:
    204         return False
    205     for byte in name.as_bytes():
    206         if not _token_byte(Int(byte)):
    207             return False
    208     return True
    209 
    210 
    211 def _valid_header_value(value: String) -> Bool:
    212     for byte in value.as_bytes():
    213         var b = Int(byte)
    214         if b == 9:
    215             continue
    216         if b < 32 or b == 127:
    217             return False
    218     return True
    219 
    220 
    221 def _trim_ows(value: String) -> String:
    222     """Trim only legal HTTP optional whitespace (SP / HTAB)."""
    223     var start = 0
    224     var end = value.byte_length()
    225     var bytes = value.as_bytes()
    226     while start < end and (Int(bytes[start]) == 32 or Int(bytes[start]) == 9):
    227         start += 1
    228     while end > start and (
    229         Int(bytes[end - 1]) == 32 or Int(bytes[end - 1]) == 9
    230     ):
    231         end -= 1
    232     if start == 0 and end == value.byte_length():
    233         return String(value)
    234     return String(value[byte=start:end])
    235 
    236 
    237 def _ascii_digits(value: String) -> Bool:
    238     if value.byte_length() == 0:
    239         return False
    240     for byte in value.as_bytes():
    241         var b = Int(byte)
    242         if b < 48 or b > 57:
    243             return False
    244     return True
    245 
    246 
    247 def _bytes_string(bytes: List[UInt8], stop: Int) raises -> String:
    248     return String(from_utf8=Span(ptr=bytes.unsafe_ptr(), length=stop))
    249 
    250 
    251 struct ConnectionReader(Movable):
    252     var _stream: TcpStream
    253     var _buffer: List[UInt8]
    254     var _eof: Bool
    255 
    256     def __init__(out self, var stream: TcpStream):
    257         self._stream = stream^
    258         self._buffer = List[UInt8]()
    259         self._eof = False
    260 
    261     def has_buffered(self) -> Bool:
    262         return len(self._buffer) > 0
    263 
    264     def _read_more(mut self) raises -> Int:
    265         var chunk = InlineArray[Byte, 4096](fill=0)
    266         var n = self._stream.read(chunk.unsafe_ptr(), 4096)
    267         if n <= 0:
    268             self._eof = True
    269             return 0
    270         for index in range(Int(n)):
    271             self._buffer.append(UInt8(Int(chunk[index])))
    272         return Int(n)
    273 
    274     def write_all(mut self, text: String) raises:
    275         self._stream.write_all(Span[UInt8, _](text.as_bytes()))
    276 
    277     def _header_end(self) -> Int:
    278         var i = 0
    279         while i + 3 < len(self._buffer):
    280             if (
    281                 self._buffer[i] == 13
    282                 and self._buffer[i + 1] == 10
    283                 and self._buffer[i + 2] == 13
    284                 and self._buffer[i + 3] == 10
    285             ):
    286                 return i
    287             i += 1
    288         return -1
    289 
    290     def read(mut self) raises -> FramedRequest:
    291         var outcome = FramedRequest(
    292             ok=False,
    293             error="",
    294             method="",
    295             path="",
    296             version="",
    297             headers_raw="",
    298             body="",
    299             content_length=0,
    300             keep_alive=False,
    301             total_bytes=0,
    302         )
    303         var header_end = self._header_end()
    304         while header_end < 0:
    305             if self._eof:
    306                 outcome.error = (
    307                     "empty" if len(self._buffer)
    308                     == 0 else "missing_header_terminator"
    309                 )
    310                 return outcome^
    311             if len(self._buffer) > STRICT_MAX_HEADER_BYTES:
    312                 outcome.error = "header_too_large"
    313                 return outcome^
    314             _ = self._read_more()
    315             # Recompute the terminator before any total-length check: a single
    316             # coalesced chunk may carry a small header plus a large body, and
    317             # only the header bytes count against the header cap.
    318             header_end = self._header_end()
    319             if header_end < 0 and len(self._buffer) > STRICT_MAX_HEADER_BYTES:
    320                 outcome.error = "header_too_large"
    321                 return outcome^
    322         if header_end > STRICT_MAX_HEADER_BYTES:
    323             outcome.error = "header_too_large"
    324             return outcome^
    325 
    326         var header_text = _bytes_string(self._buffer, header_end)
    327         var lines = header_text.split("\r\n")
    328         if len(lines) < 1:
    329             outcome.error = "malformed_request_line"
    330             return outcome^
    331         var request_line = String(lines[0])
    332         var parts = request_line.split(" ")
    333         if len(parts) != 3:
    334             outcome.error = "malformed_request_line"
    335             return outcome^
    336         outcome.method = String(parts[0])
    337         outcome.path = String(parts[1])
    338         outcome.version = String(parts[2])
    339         if not _valid_method(outcome.method):
    340             outcome.error = "malformed_method"
    341             return outcome^
    342         if not _valid_version(outcome.version):
    343             outcome.error = "malformed_version"
    344             return outcome^
    345         if outcome.path.byte_length() == 0:
    346             outcome.error = "malformed_target"
    347             return outcome^
    348 
    349         var content_length = 0
    350         var have_length = False
    351         var have_transfer = False
    352         for i in range(1, len(lines)):
    353             var line = String(lines[i])
    354             if line.byte_length() == 0:
    355                 continue
    356             var first = Int(line.as_bytes()[0])
    357             if first == 32 or first == 9:
    358                 outcome.error = "malformed_header"
    359                 return outcome^
    360             var colon = line.find(":")
    361             if colon <= 0:
    362                 outcome.error = "malformed_header"
    363                 return outcome^
    364             var name = String(line[byte=0:colon])
    365             var raw_value = String(line[byte = colon + 1 :])
    366             if not _valid_header_name(name):
    367                 outcome.error = "malformed_header"
    368                 return outcome^
    369             var value = _trim_ows(raw_value)
    370             if not _valid_header_value(value):
    371                 outcome.error = "malformed_header"
    372                 return outcome^
    373             var lower = name.lower()
    374             if lower == "content-length":
    375                 if have_length:
    376                     outcome.error = "duplicate_content_length"
    377                     return outcome^
    378                 if not _ascii_digits(value):
    379                     outcome.error = "malformed_content_length"
    380                     return outcome^
    381                 if value.byte_length() > 10:
    382                     outcome.error = "content_length_overflow"
    383                     return outcome^
    384                 var parsed = Int(value)
    385                 if parsed > STRICT_MAX_BODY_BYTES:
    386                     outcome.error = "body_too_large"
    387                     return outcome^
    388                 content_length = parsed
    389                 have_length = True
    390             elif lower == "transfer-encoding":
    391                 if have_transfer:
    392                     outcome.error = "duplicate_transfer_encoding"
    393                     return outcome^
    394                 have_transfer = True
    395                 if value.lower() != "identity":
    396                     outcome.error = "unsupported_transfer_encoding"
    397                     return outcome^
    398             elif lower == "connection" and value.lower() == "keep-alive":
    399                 outcome.keep_alive = True
    400 
    401         if have_transfer and have_length:
    402             outcome.error = "conflicting_framing"
    403             return outcome^
    404         if not have_length:
    405             content_length = 0
    406         outcome.content_length = content_length
    407 
    408         var expected_total = header_end + 4 + content_length
    409         while len(self._buffer) < expected_total:
    410             if self._eof:
    411                 outcome.error = "premature_eof"
    412                 return outcome^
    413             _ = self._read_more()
    414 
    415         outcome.headers_raw = header_text
    416         outcome.body = String(
    417             _bytes_string(self._buffer, expected_total)[byte = header_end + 4 :]
    418         )
    419         var surplus = List[UInt8]()
    420         for index in range(expected_total, len(self._buffer)):
    421             surplus.append(self._buffer[index])
    422         self._buffer = surplus^
    423         outcome.ok = True
    424         outcome.total_bytes = expected_total
    425         return outcome^
    426 
    427     def probe_completion(mut self, grace_ms: Int) raises -> String:
    428         """Bounded completion handshake after the final expected exchange.
    429 
    430         Any already-buffered or subsequently received bytes mean the client
    431         sent an extra exchange. A bounded timeout with no bytes is success; a
    432         real read error is propagated as the exact read cause (never read as
    433         success or as a timeout) and is recorded by the serve loop as a
    434         bounded io_error. The Mojo error model cannot discriminate these
    435         struct payloads by handler type, so the timeout is identified from the
    436         rendered cause.
    437         """
    438         if len(self._buffer) > 0:
    439             return "extra_exchange_after_completion"
    440         self._stream.set_recv_timeout(grace_ms)
    441         var outcome = ""
    442         try:
    443             var n = self._read_more()
    444             if n > 0:
    445                 outcome = "extra_exchange_after_completion"
    446         except e:
    447             var text = String(e)
    448             if text.startswith("Timeout"):
    449                 return ""
    450             raise Error("probe_completion_read_error:" + text)
    451         return outcome^
    452 
    453 
    454 # ── Scripted exchanges (FX01/FX02/FX04/FX05) ────────────────────────────────
    455 
    456 
    457 @fieldwise_init
    458 struct ExchangeScript(Copyable, Movable):
    459     var case_label: String
    460     var method: String
    461     var path: String
    462     var headers: String
    463     var body: String
    464     var check_body: Bool
    465     var require_bearer: Bool
    466     var status: Int
    467     var response_headers: String
    468     var response_body: String
    469     var raw_response: String
    470     var delay_ms: Int
    471     var stall_after_head_ms: Int
    472     var close_connection: Bool
    473     var echo_authorization: Bool
    474     var expect_peer_close: Bool
    475     var expected_close_cause: String
    476     var expected_close_phase: String
    477     var inject_write_error: String
    478     var inject_write_errno: Int
    479     var close_before_response: Bool
    480 
    481     def __copyinit__(out self, existing: Self):
    482         self.case_label = existing.case_label
    483         self.method = existing.method
    484         self.path = existing.path
    485         self.headers = existing.headers
    486         self.body = existing.body
    487         self.check_body = existing.check_body
    488         self.require_bearer = existing.require_bearer
    489         self.status = existing.status
    490         self.response_headers = existing.response_headers
    491         self.response_body = existing.response_body
    492         self.raw_response = existing.raw_response
    493         self.delay_ms = existing.delay_ms
    494         self.stall_after_head_ms = existing.stall_after_head_ms
    495         self.close_connection = existing.close_connection
    496         self.echo_authorization = existing.echo_authorization
    497         self.expect_peer_close = existing.expect_peer_close
    498         self.expected_close_cause = existing.expected_close_cause
    499         self.expected_close_phase = existing.expected_close_phase
    500         self.inject_write_error = existing.inject_write_error
    501         self.inject_write_errno = existing.inject_write_errno
    502         self.close_before_response = existing.close_before_response
    503 
    504 
    505 def exchange_script(
    506     case_label: String,
    507     method: String,
    508     path: String,
    509     status: Int,
    510     body: String,
    511 ) -> ExchangeScript:
    512     return ExchangeScript(
    513         case_label=case_label,
    514         method=method,
    515         path=path,
    516         headers="",
    517         body="",
    518         check_body=False,
    519         require_bearer=False,
    520         status=status,
    521         response_headers="",
    522         response_body=body,
    523         raw_response="",
    524         delay_ms=0,
    525         stall_after_head_ms=0,
    526         close_connection=True,
    527         echo_authorization=False,
    528         expect_peer_close=False,
    529         expected_close_cause="",
    530         expected_close_phase="",
    531         inject_write_error="",
    532         inject_write_errno=-1,
    533         close_before_response=False,
    534     )
    535 
    536 
    537 def verify_exchange(script: ExchangeScript, framed: FramedRequest) -> String:
    538     """Return ``""`` when the request matches the script, else a bounded reason.
    539     """
    540     if framed.method != script.method:
    541         return "method_mismatch"
    542     if framed.path != script.path:
    543         return "path_mismatch"
    544     if script.check_body and framed.body != script.body:
    545         return "body_mismatch"
    546     var expected_headers = script.headers.split("\n")
    547     for entry in expected_headers:
    548         var expected = String(entry).strip()
    549         if expected.byte_length() == 0:
    550             continue
    551         var split = expected.find(":")
    552         if split <= 0:
    553             return "malformed_script_header"
    554         var name = String(expected[byte=0:split])
    555         if name.byte_length() == 0 or not _valid_header_name(name):
    556             return "malformed_script_header"
    557         var value = _trim_ows(String(expected[byte = split + 1 :]))
    558         var values = header_values(framed.headers_raw, name)
    559         if len(values) == 0:
    560             return "header_missing:" + name
    561         if len(values) > 1:
    562             return "header_duplicate:" + name
    563         if values[0] != value:
    564             return "header_mismatch:" + name
    565     var auth = authorization_reason(framed.headers_raw, script.require_bearer)
    566     if auth != "":
    567         return auth
    568     return ""
    569 
    570 
    571 def _substitute(
    572     text: String, request_index: Int, connection_index: Int
    573 ) -> String:
    574     var out = text.replace("{request_index}", String(request_index))
    575     return out.replace("{connection_index}", String(connection_index))
    576 
    577 
    578 def render_response(
    579     script: ExchangeScript,
    580     request_headers_raw: String,
    581     request_index: Int,
    582     connection_index: Int,
    583 ) -> String:
    584     if script.raw_response != "":
    585         return String(script.raw_response)
    586     var reason = "OK"
    587     if script.status == 401:
    588         reason = "Unauthorized"
    589     elif script.status == 404:
    590         reason = "Not Found"
    591     elif script.status == 429:
    592         reason = "Too Many Requests"
    593     elif script.status == 500:
    594         reason = "Internal Server Error"
    595     elif script.status == 503:
    596         reason = "Service Unavailable"
    597     elif script.status == 529:
    598         reason = "Overloaded"
    599     var body = _substitute(
    600         script.response_body, request_index, connection_index
    601     )
    602     if script.echo_authorization:
    603         body = (
    604             '{"authorization":'
    605             + json_escape(bearer_token(request_headers_raw))
    606             + "}"
    607         )
    608     var extra = ""
    609     if script.response_headers != "":
    610         extra = script.response_headers + "\r\n"
    611     var connection = "keep-alive" if not script.close_connection else "close"
    612     return (
    613         "HTTP/1.1 "
    614         + String(script.status)
    615         + " "
    616         + reason
    617         + "\r\ncontent-type: application/json\r\n"
    618         + extra
    619         + "content-length: "
    620         + String(body.byte_length())
    621         + "\r\nconnection: "
    622         + connection
    623         + "\r\n\r\n"
    624         + body
    625     )
    626 
    627 
    628 # ── Convenience-mode validation + reports ────────────────────────────────────
    629 
    630 
    631 def classify_write_error_cause(text: String) -> String:
    632     """Bounded cause class for a response-write error observed by the fixture.
    633 
    634     Mojo's error model cannot discriminate handler types, so the exact rendered
    635     cause is classified. A script that declares an expected peer close must
    636     match one of these bounded classes *and* the exact phase; every other write
    637     error (an injected handler failure, a write timeout, an invalid descriptor)
    638     is surfaced as a bounded fixture failure rather than tolerated.
    639     """
    640     if text.find("injected_error") >= 0:
    641         # The bounded test-only write-error seam is never a real peer close, so
    642         # an injected error can never be accepted by declaring a peer-close
    643         # cause (ADR-0020 TC01).
    644         return "unrelated_error"
    645     if text.find("Bad file descriptor") >= 0 or text.find("(errno 9)") >= 0:
    646         return "invalid_descriptor"
    647     if text.find("ConnectionReset") >= 0:
    648         return "peer_reset"
    649     if text.find("BrokenPipe") >= 0:
    650         return "broken_pipe"
    651     if text.find("Timeout") >= 0:
    652         return "write_timeout"
    653     return "unrelated_error"
    654 
    655 
    656 def write_errno_class(errno_value: Int) -> String:
    657     """Bounded class for a real write(2)/send(2) errno.
    658 
    659     Mirrors the flare ``TcpStream.write`` mapping, so synthetic seams and real
    660     failures are classified by the same vocabulary and a timeout or descriptor
    661     error can be compared directly against a declared peer-close cause.
    662     """
    663     if errno_value == Int(ErrNo.EPIPE.value):
    664         return "broken_pipe"
    665     if errno_value == Int(ErrNo.ECONNRESET.value):
    666         return "peer_reset"
    667     if errno_value == Int(ErrNo.EAGAIN.value) or errno_value == Int(
    668         ErrNo.EWOULDBLOCK.value
    669     ):
    670         return "write_timeout"
    671     if errno_value == Int(ErrNo.EBADF.value):
    672         return "invalid_descriptor"
    673     return "unrelated_error"
    674 
    675 
    676 def render_write_api_error(errno_value: Int) -> String:
    677     """Render an errno exactly as the flare write API would render it.
    678 
    679     Used by the narrowly scoped syscall-result seam so the synthetic failure is
    680     classified by the *same* path as a real write failure, and by the real
    681     descriptor control to render the observed errno. It is a rendering helper
    682     only and changes no product or fork source.
    683     """
    684     if errno_value == Int(ErrNo.EAGAIN.value) or errno_value == Int(
    685         ErrNo.EWOULDBLOCK.value
    686     ):
    687         return "Timeout: send"
    688     if errno_value == Int(ErrNo.EPIPE.value):
    689         return "BrokenPipe"
    690     if errno_value == Int(ErrNo.ECONNRESET.value):
    691         return "ConnectionReset"
    692     if errno_value == Int(ErrNo.EBADF.value):
    693         return "NetworkError(errno 9): Bad file descriptor (send)"
    694     return "NetworkError(errno " + String(errno_value) + "): send error"
    695 
    696 
    697 def is_peer_close_cause(cause: String) -> Bool:
    698     """True only for the bounded peer-close classes a script may expect.
    699 
    700     A declaration is structurally restricted to an actual peer close, so a
    701     write timeout, an invalid descriptor or an unrelated handler error can
    702     never be waived by naming it as the expected cause (ADR-0020 TC01).
    703     """
    704     return cause == "peer_reset" or cause == "broken_pipe"
    705 
    706 
    707 def is_expected_close_phase(phase: String) -> Bool:
    708     """True only for a write step the fixture can actually be performing.
    709 
    710     EC01: an expected-close declaration must name an observed phase, so an
    711     unknown or empty phase is an invalid declaration and cannot be satisfied by
    712     any real write step.
    713     """
    714     return (
    715         phase == "delayed_write"
    716         or phase == "head_write"
    717         or phase == "body_stall"
    718     )
    719 
    720 
    721 def is_valid_close_declaration(script: ExchangeScript) -> Bool:
    722     """True when an expected-close declaration names a real cause and phase.
    723 
    724     EC01: an invalid cause/phase declaration is rejected before any response
    725     work, so a script can never succeed merely because response writing
    726     happened to raise an unrelated error.
    727     """
    728     return is_peer_close_cause(
    729         script.expected_close_cause
    730     ) and is_expected_close_phase(script.expected_close_phase)
    731 
    732 
    733 def serve_scripts(
    734     listener: TcpListener, var scripts: List[ExchangeScript], label: String
    735 ) raises -> ServeReport:
    736     """Serve an ordered list of explicit exchange scripts (FX01/FX02/FX05).
    737 
    738     Every script must be consumed in order on its expected method/path/
    739     selected-headers/body; unexpected, extra, missing and unconsumed exchanges
    740     fail with a bounded case-specific reason. A 404-then-success outcome is
    741     never a pass.
    742     """
    743     var request_count = 0
    744     var connection_count = 0
    745     var total = len(scripts)
    746     var accepted_closes = List[String]()
    747     try:
    748         while request_count < total:
    749             var stream = listener.accept()
    750             connection_count += 1
    751             var reader = ConnectionReader(stream^)
    752             var close_connection = False
    753             while request_count < total:
    754                 var framed = reader.read()
    755                 if not framed.ok:
    756                     if framed.error == "empty":
    757                         if request_count < total:
    758                             return ServeReport(
    759                                 False,
    760                                 "accounting",
    761                                 label,
    762                                 "missing_exchanges",
    763                                 request_count,
    764                                 connection_count,
    765                             )
    766                         break
    767                     return ServeReport(
    768                         False,
    769                         "read",
    770                         label,
    771                         framed.error,
    772                         request_count,
    773                         connection_count,
    774                     )
    775                 var script = scripts[request_count].copy()
    776                 var verify = verify_exchange(script, framed)
    777                 if verify != "":
    778                     return ServeReport(
    779                         False,
    780                         "exchange",
    781                         script.case_label,
    782                         verify,
    783                         request_count,
    784                         connection_count,
    785                     )
    786                 # EC01: reject an invalid expected-close declaration before any
    787                 # response work, even when the write would have succeeded.
    788                 if script.expect_peer_close and not is_valid_close_declaration(
    789                     script
    790                 ):
    791                     return ServeReport(
    792                         False,
    793                         "declaration",
    794                         script.case_label,
    795                         "invalid_expected_close_declaration",
    796                         request_count,
    797                         connection_count,
    798                     )
    799                 var next_index = request_count + 1
    800                 if next_index == total:
    801                     var extra = reader.probe_completion(
    802                         STRICT_COMPLETION_GRACE_MS
    803                     )
    804                     if extra != "":
    805                         return ServeReport(
    806                             False,
    807                             "accounting",
    808                             script.case_label,
    809                             extra,
    810                             request_count,
    811                             connection_count,
    812                         )
    813                 request_count = next_index
    814                 if script.close_before_response:
    815                     # RP01 raw-observation control: a peer that closes the
    816                     # connection without sending a response head at all. The
    817                     # raw observer must report a bounded raw_eof rather than
    818                     # hang or invent a status.
    819                     close_connection = True
    820                     break
    821                 if script.delay_ms > 0:
    822                     usleep(script.delay_ms * 1000)
    823                 var phase = "delayed_write"
    824                 try:
    825                     if script.stall_after_head_ms > 0:
    826                         # Headers-then-stall control (H007): write the complete
    827                         # response head, flush it, hold the connection open for
    828                         # the declared bounded stall, then attempt the body. This
    829                         # separates a body-read stall from a connection timeout
    830                         # and from the overall budget using a bounded fixture
    831                         # delay that never hangs the owning test. ``phase`` is
    832                         # the write step actually being attempted, not a script
    833                         # label, so a head-write failure is not reported as a
    834                         # body stall.
    835                         var rendered = render_response(
    836                             script,
    837                             framed.headers_raw,
    838                             request_count,
    839                             connection_count,
    840                         )
    841                         var separator = rendered.find("\r\n\r\n")
    842                         if separator >= 0:
    843                             var head_end = separator + 4
    844                             phase = "head_write"
    845                             reader.write_all(String(rendered[byte=0:head_end]))
    846                             phase = "body_stall"
    847                             usleep(script.stall_after_head_ms * 1000)
    848                             if script.inject_write_error != "":
    849                                 raise Error(
    850                                     "injected_error: "
    851                                     + script.inject_write_error
    852                                 )
    853                             if script.inject_write_errno >= 0:
    854                                 raise Error(
    855                                     "synthetic_errno: "
    856                                     + render_write_api_error(
    857                                         script.inject_write_errno
    858                                     )
    859                                 )
    860                             reader.write_all(String(rendered[byte=head_end:]))
    861                         else:
    862                             phase = "head_write"
    863                             reader.write_all(rendered)
    864                     else:
    865                         phase = "delayed_write"
    866                         if script.inject_write_error != "":
    867                             raise Error(
    868                                 "injected_error: " + script.inject_write_error
    869                             )
    870                         if script.inject_write_errno >= 0:
    871                             raise Error(
    872                                 "synthetic_errno: "
    873                                 + render_write_api_error(
    874                                     script.inject_write_errno
    875                                 )
    876                             )
    877                         reader.write_all(
    878                             render_response(
    879                                 script,
    880                                 framed.headers_raw,
    881                                 request_count,
    882                                 connection_count,
    883                             )
    884                         )
    885                 except e:
    886                     # A delay/stall is not permission to swallow every write
    887                     # error. Only an explicitly declared expected peer close
    888                     # whose exact bounded cause and phase match is accepted;
    889                     # unexpected/wrong-phase closes, write timeouts, invalid
    890                     # descriptors and unrelated handler errors fail the fixture
    891                     # with a bounded, cause-specific reason. The declared cause
    892                     # is itself restricted to a real peer-close class, so a
    893                     # timeout or unrelated error cannot be waived by declaring
    894                     # it as the expected cause.
    895                     # Record the raw errno observed here as well as the
    896                     # classification. A synthetic seam is labelled explicitly
    897                     # so it is never presented as a real OS timeout/EBADF.
    898                     var raw_errno = Int(get_errno().value)
    899                     var observed_text = String(e)
    900                     var synthetic = (
    901                         observed_text.find("synthetic_errno") >= 0
    902                         or observed_text.find("injected_error") >= 0
    903                     )
    904                     var observed = classify_write_error_cause(observed_text)
    905                     var evidence = (
    906                         "synthdecl"
    907                         + String(
    908                             script.inject_write_errno
    909                         ) if synthetic else "realerrno"
    910                         + String(raw_errno)
    911                     )
    912                     # OB03: provenance is kept separate from the error class. A
    913                     # synthetic/injected seam is never a real peer close, so it
    914                     # cannot satisfy an expected real close even when it maps to
    915                     # the same bounded class (EPIPE -> broken_pipe, ECONNRESET ->
    916                     # peer_reset). It is rejected here with its synthetic evidence
    917                     # label instead of being accepted.
    918                     if (
    919                         script.expect_peer_close
    920                         and not synthetic
    921                         and is_peer_close_cause(script.expected_close_cause)
    922                         and observed == script.expected_close_cause
    923                         and phase == script.expected_close_phase
    924                     ):
    925                         # EC01: a permitted close consumes this exchange only.
    926                         # The connection is finished, so the sequence continues
    927                         # on a fresh connection and the exact request-count
    928                         # accounting still requires every remaining script.
    929                         accepted_closes.append(
    930                             script.case_label
    931                             + "_peer_close_"
    932                             + observed
    933                             + "_"
    934                             + phase
    935                             + "_"
    936                             + evidence
    937                         )
    938                         close_connection = True
    939                     else:
    940                         return ServeReport(
    941                             False,
    942                             "peer_close",
    943                             script.case_label,
    944                             "unexpected_write_"
    945                             + observed
    946                             + "_"
    947                             + phase
    948                             + "_"
    949                             + evidence,
    950                             request_count,
    951                             connection_count,
    952                         )
    953                 else:
    954                     # EC01: the script declared an expected peer close, but the
    955                     # response write succeeded, so the declared event never
    956                     # happened. A successful write is not permission to accept
    957                     # a declared close that was never observed.
    958                     if script.expect_peer_close:
    959                         return ServeReport(
    960                             False,
    961                             "peer_close",
    962                             script.case_label,
    963                             "missing_expected_close_"
    964                             + script.expected_close_phase,
    965                             request_count,
    966                             connection_count,
    967                         )
    968                 if script.close_connection or close_connection:
    969                     break
    970         if request_count < total:
    971             return ServeReport(
    972                 False,
    973                 "accounting",
    974                 scripts[request_count].case_label,
    975                 "missing_exchanges",
    976                 request_count,
    977                 connection_count,
    978             )
    979         var case_label = label
    980         if len(accepted_closes) > 0:
    981             case_label = ""
    982             for index in range(len(accepted_closes)):
    983                 if index > 0:
    984                     case_label += ";"
    985                 case_label += accepted_closes[index]
    986         return ServeReport(
    987             True, "complete", case_label, "ok", request_count, connection_count
    988         )
    989     except:
    990         return ServeReport(
    991             False, "read", label, "io_error", request_count, connection_count
    992         )
    993 
    994 
    995 def validate_convenience(
    996     framed: FramedRequest,
    997     allowed_paths: List[String],
    998     allowed_methods: List[String],
    999     require_bearer: Bool,
   1000 ) -> String:
   1001     """Validate route then method (and auth) before any response.
   1002 
   1003     ``allowed_methods`` is parallel to ``allowed_paths``. Wrong routes,
   1004     methods and spoofed auth never count as a successful intended exchange
   1005     (FX02/FX04).
   1006     """
   1007     var matched = -1
   1008     for index in range(len(allowed_paths)):
   1009         if framed.path == allowed_paths[index]:
   1010             matched = index
   1011     if matched < 0:
   1012         return "unexpected_path"
   1013     if matched >= len(allowed_methods):
   1014         return "unexpected_method"
   1015     if framed.method != allowed_methods[matched]:
   1016         return "unexpected_method"
   1017     return authorization_reason(framed.headers_raw, require_bearer)
   1018 
   1019 
   1020 @fieldwise_init
   1021 struct ServeReport(Movable):
   1022     var ok: Bool
   1023     var phase: String
   1024     var case_label: String
   1025     var reason: String
   1026     var requests: Int
   1027     var connections: Int
   1028 
   1029     def describe(self) -> String:
   1030         return (
   1031             "phase="
   1032             + self.phase
   1033             + " case="
   1034             + self.case_label
   1035             + " reason="
   1036             + self.reason
   1037             + " requests="
   1038             + String(self.requests)
   1039             + " connections="
   1040             + String(self.connections)
   1041         )
   1042 
   1043 
   1044 def report_status_matches_exit(
   1045     exited: Bool, exit_code: Int, report_ok: Bool
   1046 ) -> Bool:
   1047     """The fixture child exits 0 for a successful report and 125 for a failed
   1048     one; any other exit (including a signal) contradicts the report."""
   1049     if not exited:
   1050         return False
   1051     if report_ok:
   1052         return exit_code == 0
   1053     return exit_code == 125
   1054 
   1055 
   1056 def report_line(report: ServeReport) -> String:
   1057     var state = "ok" if report.ok else "fail"
   1058     return "result " + state + " " + report.describe()
   1059 
   1060 
   1061 def _strict_nonneg(value: String) -> Int:
   1062     """Parse a nonnegative decimal count; -1 for empty/non-digit/overflow."""
   1063     if value.byte_length() == 0 or value.byte_length() > 12:
   1064         return -1
   1065     var total = 0
   1066     for byte in value.as_bytes():
   1067         var b = Int(byte)
   1068         if b < 48 or b > 57:
   1069             return -1
   1070         total = total * 10 + (b - 48)
   1071     return total
   1072 
   1073 
   1074 def _report_failure(reason: String) -> ServeReport:
   1075     return ServeReport(False, "parse", "-", reason, -1, -1)
   1076 
   1077 
   1078 def parse_report(line: String) -> ServeReport:
   1079     """Strictly parse one bounded ``result`` report line.
   1080 
   1081     Missing/truncated/duplicate/malformed/unknown/missing-count fields fail
   1082     with a bounded cause instead of defaulting counts to zero, and only the
   1083     exact ``ok``/``fail`` status is accepted.
   1084     """
   1085     if not line.startswith("result "):
   1086         return _report_failure("missing_result_prefix")
   1087     var parts = String(line[byte=7:]).split(" ")
   1088     if len(parts) == 0:
   1089         return _report_failure("missing_status")
   1090     var status = String(parts[0])
   1091     if status != "ok" and status != "fail":
   1092         return _report_failure("unknown_status")
   1093     var phase = ""
   1094     var case_label = ""
   1095     var reason = ""
   1096     var requests = -1
   1097     var connections = -1
   1098     var have_phase = False
   1099     var have_case = False
   1100     var have_reason = False
   1101     var have_requests = False
   1102     var have_connections = False
   1103     for index in range(1, len(parts)):
   1104         var field = String(parts[index])
   1105         if field.byte_length() == 0:
   1106             return _report_failure("empty_field")
   1107         var eq = field.find("=")
   1108         if eq <= 0:
   1109             return _report_failure("malformed_field")
   1110         var key = String(field[byte=0:eq])
   1111         var value = String(field[byte = eq + 1 :])
   1112         if value.byte_length() == 0:
   1113             return _report_failure("empty_field")
   1114         if key == "phase":
   1115             if have_phase:
   1116                 return _report_failure("duplicate_field")
   1117             have_phase = True
   1118             phase = value
   1119         elif key == "case":
   1120             if have_case:
   1121                 return _report_failure("duplicate_field")
   1122             have_case = True
   1123             case_label = value
   1124         elif key == "reason":
   1125             if have_reason:
   1126                 return _report_failure("duplicate_field")
   1127             have_reason = True
   1128             reason = value
   1129         elif key == "requests":
   1130             if have_requests:
   1131                 return _report_failure("duplicate_field")
   1132             have_requests = True
   1133             requests = _strict_nonneg(value)
   1134             if requests < 0:
   1135                 return _report_failure("invalid_count")
   1136         elif key == "connections":
   1137             if have_connections:
   1138                 return _report_failure("duplicate_field")
   1139             have_connections = True
   1140             connections = _strict_nonneg(value)
   1141             if connections < 0:
   1142                 return _report_failure("invalid_count")
   1143         else:
   1144             return _report_failure("unknown_field")
   1145     if not (
   1146         have_phase
   1147         and have_case
   1148         and have_reason
   1149         and have_requests
   1150         and have_connections
   1151     ):
   1152         return _report_failure("missing_field")
   1153     if status == "ok" and (phase != "complete" or reason != "ok"):
   1154         # A claimed success must carry the emitted success phase/reason; any
   1155         # other pairing is an inconsistent report, never a success.
   1156         return _report_failure("inconsistent_status")
   1157     return ServeReport(
   1158         status == "ok", phase, case_label, reason, requests, connections
   1159     )