feat(http): Http.* poll-based HTTP/HTTPS client over a native NSURLConnection backend (#6)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 21s

Adds the Http.* namespace and its transport, the HTTP client from #6. The JSON
companion the proposal called for already shipped as Json.* (#44).

- runtime/native/http.ll: the macOS transport — NSURLConnection driven through
  the objc runtime C ABI (no ObjC/C source), run on a detached pthread so the
  frame never blocks; TLS is the system's, on by default. A fixed slot pool holds
  each in-flight request; the worker publishes status/body/response behind an
  atomic done flag (release/acquire). Spliced + Foundation linked only when a
  program uses Http.*.
- runtime/native/http.ludic: the Http.* runtime — get/post/request, the
  open/set/body/send builder, poll/status/ok/text/body_len/header/free, plus a
  pure-Ludic response parser (Http.parse + case-insensitive header lookup) that
  is transport-independent and portable.
- compiler: Http.* dispatch, g_uses_http splice, hs_* transport intrinsics +
  declarations, the conditional Foundation link, and a new \r string/char escape
  the protocol needs.
- docs: a full docs/language/http section (16 pages); check-impl/check-docs green.
- test: examples/library/http.ludic self-asserts the parser offline (Darwin-gated
  build via the canonical path, since it links Foundation).

Verified end to end against real endpoints: HTTPS GET (200 + headers + body) and
POST (body + custom header). HTTP is out-of-band and never feeds the
deterministic sim. Reseeded; suites green (81 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 18:10:53 +03:00
parent 4eef5ebbce
commit 3df6640fa5
29 changed files with 24936 additions and 22396 deletions

306
runtime/native/http.ll Normal file
View file

@ -0,0 +1,306 @@
; ============================================================================
; http.ll — the macOS HTTP transport, written in LLVM IR.
;
; Like cocoa.ll / audio.ll, no Objective-C and no C source: this drives
; NSURLConnection through the objc runtime's C ABI, and runs the (blocking)
; request on a detached pthread so the game's frame loop never stalls. TLS is
; the system's, on by default with certificate verification — an https:// URL
; just works. ludicc splices this in and links Foundation only when a program
; uses Http.*.
;
; A small fixed pool of slots (0..HTTP_SLOTS-1) each hold one in-flight request:
; the pending NSURLRequest, the worker's pthread id, and — once the worker
; finishes — the status code, a malloc'd copy of the body bytes, and the
; retained NSHTTPURLResponse (so header queries can be answered later). The done
; flag is an atomic released by the worker and acquired by the poller, so the
; body / status / response the poller reads are the ones the worker wrote.
;
; Exposed to Ludic (through the http_* intrinsics):
; http_req_new(method, url) -> req http_req_header(req, name, val)
; http_req_body(req, bytes, len) http_send(slot, req)
; http_done(slot) -> bool http_status(slot) -> int
; http_body(slot) -> ptr http_blen(slot) -> int
; http_header(slot, name) -> cstr http_free(slot)
; ============================================================================
declare ptr @objc_getClass(ptr)
declare ptr @sel_registerName(ptr)
declare ptr @objc_msgSend(ptr, ptr, ...)
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare i32 @pthread_create(ptr, ptr, ptr, ptr)
declare i32 @pthread_detach(ptr)
@.h_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00"
@.h_nsstr = private unnamed_addr constant [9 x i8] c"NSString\00"
@.h_nsdata = private unnamed_addr constant [7 x i8] c"NSData\00"
@.h_murl = private unnamed_addr constant [20 x i8] c"NSMutableURLRequest\00"
@.h_conn = private unnamed_addr constant [16 x i8] c"NSURLConnection\00"
@.h_pool = private unnamed_addr constant [18 x i8] c"NSAutoreleasePool\00"
@.h_urlws = private unnamed_addr constant [15 x i8] c"URLWithString:\00"
@.h_utf8 = private unnamed_addr constant [22 x i8] c"stringWithUTF8String:\00"
@.h_reqw = private unnamed_addr constant [16 x i8] c"requestWithURL:\00"
@.h_smeth = private unnamed_addr constant [15 x i8] c"setHTTPMethod:\00"
@.h_shdr = private unnamed_addr constant [29 x i8] c"setValue:forHTTPHeaderField:\00"
@.h_sbody = private unnamed_addr constant [13 x i8] c"setHTTPBody:\00"
@.h_dwbl = private unnamed_addr constant [22 x i8] c"dataWithBytes:length:\00"
@.h_bytes = private unnamed_addr constant [6 x i8] c"bytes\00"
@.h_length = private unnamed_addr constant [7 x i8] c"length\00"
@.h_ssync = private unnamed_addr constant [48 x i8] c"sendSynchronousRequest:returningResponse:error:\00"
@.h_status = private unnamed_addr constant [11 x i8] c"statusCode\00"
@.h_vhdr = private unnamed_addr constant [25 x i8] c"valueForHTTPHeaderField:\00"
@.h_utf8g = private unnamed_addr constant [11 x i8] c"UTF8String\00"
@.h_alloc = private unnamed_addr constant [6 x i8] c"alloc\00"
@.h_init = private unnamed_addr constant [5 x i8] c"init\00"
@.h_drain = private unnamed_addr constant [6 x i8] c"drain\00"
@.h_retain = private unnamed_addr constant [7 x i8] c"retain\00"
@.h_rel = private unnamed_addr constant [8 x i8] c"release\00"
@.h_empty = private unnamed_addr constant [1 x i8] c"\00"
; HTTP_SLOTS = 8
@H_req = internal global [8 x ptr] zeroinitializer
@H_resp = internal global [8 x ptr] zeroinitializer
@H_body = internal global [8 x ptr] zeroinitializer
@H_blen = internal global [8 x i32] zeroinitializer
@H_status = internal global [8 x i32] zeroinitializer
@H_done = internal global [8 x i32] zeroinitializer
; --- request construction ---------------------------------------------------
; Build an NSMutableURLRequest for `method` + `url`; returns it retained.
define ptr @hs_req_new(ptr %method, ptr %url) {
entry:
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
%nsurlstr = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %url)
%urlcls = call ptr @objc_getClass(ptr @.h_nsurl)
%sel_uw = call ptr @sel_registerName(ptr @.h_urlws)
%nsurl = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %urlcls, ptr %sel_uw, ptr %nsurlstr)
%nou = icmp eq ptr %nsurl, null
br i1 %nou, label %fail, label %mk
mk:
%reqcls = call ptr @objc_getClass(ptr @.h_murl)
%sel_rw = call ptr @sel_registerName(ptr @.h_reqw)
%req = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %reqcls, ptr %sel_rw, ptr %nsurl)
%nor = icmp eq ptr %req, null
br i1 %nor, label %fail, label %setm
setm:
%mstr = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %method)
%sel_m = call ptr @sel_registerName(ptr @.h_smeth)
%rm = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_m, ptr %mstr)
; retain so it survives the caller's autorelease context until http_send.
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
%rr = call ptr (ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_ret)
ret ptr %req
fail:
ret ptr null
}
define void @hs_req_header(ptr %req, ptr %name, ptr %val) {
entry:
%nil = icmp eq ptr %req, null
br i1 %nil, label %out, label %go
go:
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
%n = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %name)
%v = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %val)
%sel_sh = call ptr @sel_registerName(ptr @.h_shdr)
%r = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_sh, ptr %v, ptr %n)
br label %out
out:
ret void
}
define void @hs_req_body(ptr %req, ptr %bytes, i32 %len) {
entry:
%nil = icmp eq ptr %req, null
br i1 %nil, label %out, label %go
go:
%datacls = call ptr @objc_getClass(ptr @.h_nsdata)
%sel_d = call ptr @sel_registerName(ptr @.h_dwbl)
%l64 = sext i32 %len to i64
%data = call ptr (ptr, ptr, ptr, i64) @objc_msgSend(ptr %datacls, ptr %sel_d, ptr %bytes, i64 %l64)
%sel_sb = call ptr @sel_registerName(ptr @.h_sbody)
%r = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_sb, ptr %data)
br label %out
out:
ret void
}
; --- the worker + dispatch --------------------------------------------------
; The detached worker: run the blocking request for one slot and publish the
; result. arg is the slot index encoded as a pointer.
define ptr @hs_worker(ptr %arg) {
entry:
%slot64 = ptrtoint ptr %arg to i64
%slot = trunc i64 %slot64 to i32
%poolcls = call ptr @objc_getClass(ptr @.h_pool)
%sel_a = call ptr @sel_registerName(ptr @.h_alloc)
%p0 = call ptr (ptr, ptr) @objc_msgSend(ptr %poolcls, ptr %sel_a)
%sel_i = call ptr @sel_registerName(ptr @.h_init)
%pool = call ptr (ptr, ptr) @objc_msgSend(ptr %p0, ptr %sel_i)
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
%req = load ptr, ptr %reqp
%respslot = alloca ptr
%errslot = alloca ptr
store ptr null, ptr %respslot
store ptr null, ptr %errslot
%conncls = call ptr @objc_getClass(ptr @.h_conn)
%sel_ss = call ptr @sel_registerName(ptr @.h_ssync)
%data = call ptr (ptr, ptr, ptr, ptr, ptr) @objc_msgSend(ptr %conncls, ptr %sel_ss, ptr %req, ptr %respslot, ptr %errslot)
%nodata = icmp eq ptr %data, null
br i1 %nodata, label %publish, label %havebody
havebody:
%sel_len = call ptr @sel_registerName(ptr @.h_length)
%blen64 = call i64 (ptr, ptr) @objc_msgSend(ptr %data, ptr %sel_len)
%blen = trunc i64 %blen64 to i32
%sel_by = call ptr @sel_registerName(ptr @.h_bytes)
%src = call ptr (ptr, ptr) @objc_msgSend(ptr %data, ptr %sel_by)
; malloc blen+1 and copy, NUL-terminating so the Ludic side can read text.
%cap = add i64 %blen64, 1
%buf = call ptr @malloc(i64 %cap)
%cp = call ptr @memcpy(ptr %buf, ptr %src, i64 %blen64)
%term = getelementptr i8, ptr %buf, i64 %blen64
store i8 0, ptr %term
%bodyp = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
store ptr %buf, ptr %bodyp
%blenp = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
store i32 %blen, ptr %blenp
br label %publish
publish:
; status + retained response (for later header queries), from the out-param.
%resp = load ptr, ptr %respslot
%noresp = icmp eq ptr %resp, null
br i1 %noresp, label %done, label %withresp
withresp:
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
%rr = call ptr (ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_ret)
%respp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
store ptr %resp, ptr %respp
%sel_st = call ptr @sel_registerName(ptr @.h_status)
%st64 = call i64 (ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_st)
%st = trunc i64 %st64 to i32
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
store i32 %st, ptr %stp
br label %done
done:
; release the request we retained in http_send, drain the pool, then publish
; the done flag last with release ordering so the reader sees the writes above.
%sel_rel = call ptr @sel_registerName(ptr @.h_rel)
%rrel = call ptr (ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_rel)
%sel_dr = call ptr @sel_registerName(ptr @.h_drain)
%dr = call ptr (ptr, ptr) @objc_msgSend(ptr %pool, ptr %sel_dr)
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
store atomic i32 1, ptr %donep release, align 4
ret ptr null
}
; Start the request on `slot` running on a detached worker thread.
define void @hs_send(i32 %slot, ptr %req) {
entry:
; reset the slot
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
store atomic i32 0, ptr %donep release, align 4
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
store i32 0, ptr %stp
%bodyp = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
store ptr null, ptr %bodyp
%blenp = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
store i32 0, ptr %blenp
%respp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
store ptr null, ptr %respp
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
store ptr %req, ptr %reqp
%tid = alloca i64
%arg = inttoptr i32 %slot to ptr
%rc = call i32 @pthread_create(ptr %tid, ptr null, ptr @hs_worker, ptr %arg)
%t = load i64, ptr %tid
%tp = inttoptr i64 %t to ptr
%d = call i32 @pthread_detach(ptr %tp)
ret void
}
; --- result accessors -------------------------------------------------------
define i32 @hs_done(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
define i32 @hs_status(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
%v = load i32, ptr %p
ret i32 %v
}
define ptr @hs_body(i32 %slot) {
entry:
%p = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
%v = load ptr, ptr %p
ret ptr %v
}
define i32 @hs_blen(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
%v = load i32, ptr %p
ret i32 %v
}
; A response header value as a C string, or "" if absent / no response yet.
define ptr @hs_header(i32 %slot, ptr %name) {
entry:
%rp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
%resp = load ptr, ptr %rp
%nores = icmp eq ptr %resp, null
br i1 %nores, label %empty, label %ask
ask:
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
%ns = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %name)
%sel_vh = call ptr @sel_registerName(ptr @.h_vhdr)
%val = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_vh, ptr %ns)
%noval = icmp eq ptr %val, null
br i1 %noval, label %empty, label %tocstr
tocstr:
%sel_g = call ptr @sel_registerName(ptr @.h_utf8g)
%c = call ptr (ptr, ptr) @objc_msgSend(ptr %val, ptr %sel_g)
ret ptr %c
empty:
ret ptr @.h_empty
}
; Free a slot's owned resources (the body copy and the retained response).
define void @hs_free(i32 %slot) {
entry:
%bp = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
%b = load ptr, ptr %bp
%nb = icmp eq ptr %b, null
br i1 %nb, label %resp, label %fb
fb:
call void @free(ptr %b)
store ptr null, ptr %bp
br label %resp
resp:
%rp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
%r = load ptr, ptr %rp
%nr = icmp eq ptr %r, null
br i1 %nr, label %out, label %rr
rr:
%sel_rel = call ptr @sel_registerName(ptr @.h_rel)
%x = call ptr (ptr, ptr) @objc_msgSend(ptr %r, ptr %sel_rel)
store ptr null, ptr %rp
br label %out
out:
ret void
}

280
runtime/native/http.ludic Normal file
View file

@ -0,0 +1,280 @@
# ============================================================================
# http.ludic — the Http.* standard library (#6).
#
# A poll-based HTTP/HTTPS client. A request is opened, optionally given headers
# and a body, then sent; each frame the game polls it, and when the reply lands
# it reads the status, body and headers — the frame never blocks (the request
# runs on a background thread in the platform backend, http.ll). TLS is the
# system's, on by default, so an https:// URL just works.
#
# HTTP is explicitly OUT-OF-BAND: it depends on the network and wall clock and
# must never feed the deterministic lockstep/replay simulation — it is for
# scores, config, telemetry and downloads, exactly like Net.* and Time.now. The
# JSON companion the proposal called for already shipped as Json.* (#44); pair
# them: Json.parse(Http.text(h)).
#
# The transport (the http_* intrinsics) is macOS-only for now; the response
# PARSER below (Http.parse and the header lookup) is pure Ludic, so it works on
# any target and is what the test suite exercises without touching the network.
# ============================================================================
const HTTP_SLOTS: int = 8
var h_ready: bool = false
var h_used: words = null # slot in use
var h_native: words = null # 1 = backed by the platform fetch, 0 = parsed offline
var h_sent: words = null # 1 = dispatched (native)
var h_res: words = null # 1 = a native fetch has been resolved into the fields below
var h_status: words = null # cached HTTP status (0 = network error / unset)
var h_blen: words = null # body length
var h_req: pointers = null # the pending NSURLRequest (native, before send)
var h_body: pointers = null # body bytes (NUL-terminated); owned by the slot
var h_hdr: pointers = null # raw header block (parsed handles only); owned by the slot
function http_init() -> void {
if h_ready { return }
h_used = words(HTTP_SLOTS)
h_native = words(HTTP_SLOTS)
h_sent = words(HTTP_SLOTS)
h_res = words(HTTP_SLOTS)
h_status = words(HTTP_SLOTS)
h_blen = words(HTTP_SLOTS)
h_req = bytes(HTTP_SLOTS * 8)
h_body = bytes(HTTP_SLOTS * 8)
h_hdr = bytes(HTTP_SLOTS * 8)
var i = 0
while i < HTTP_SLOTS {
h_used[i] = 0; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
h_status[i] = 0; h_blen[i] = 0
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
i = i + 1
}
h_ready = true
}
function http_slot_alloc() -> int {
http_init()
var i = 0
while i < HTTP_SLOTS {
if h_used[i] == 0 {
h_used[i] = 1; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
h_status[i] = 0; h_blen[i] = 0
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
return i
}
i = i + 1
}
return 0 - 1
}
function http_valid(h: int) -> bool {
http_init()
if (h < 1) or (h > HTTP_SLOTS) { return false }
return h_used[h - 1] != 0
}
# ---- length of a NUL-terminated C string -----------------------------------
function http_cstr_len(p: pointer) -> int {
if p == null { return 0 }
var n = 0
while p[n] != 0 { n = n + 1 }
return n
}
# ---- request construction (native) -----------------------------------------
# Open a request without sending it, so headers / a body can be added first.
function http_open(method: pointer, url: pointer) -> int {
let req = hs_req_new(method, url)
if req == null { return 0 }
let s = http_slot_alloc()
if s < 0 { return 0 }
h_req[s] = req
h_native[s] = 1
return s + 1
}
function http_set_header(h: int, name: pointer, value: pointer) -> void {
if not http_valid(h) { return }
let s = h - 1
if (h_sent[s] == 0) and (h_req[s] != null) { hs_req_header(h_req[s], name, value) }
}
# Attach a body of `len` bytes (use http_body for a NUL-terminated string body).
function http_body_n(h: int, bytes_ptr: pointer, len: int) -> void {
if not http_valid(h) { return }
let s = h - 1
if (h_sent[s] == 0) and (h_req[s] != null) { hs_req_body(h_req[s], bytes_ptr, len) }
}
function http_body(h: int, body: pointer) -> void {
http_body_n(h, body, http_cstr_len(body))
}
# Dispatch the request onto the background worker.
function http_send_req(h: int) -> void {
if not http_valid(h) { return }
let s = h - 1
if (h_sent[s] == 0) and (h_req[s] != null) {
hs_send(s, h_req[s])
h_sent[s] = 1
}
}
# ---- convenience one-shots -------------------------------------------------
function http_get(url: pointer) -> int {
let h = http_open("GET", url)
if h != 0 { http_send_req(h) }
return h
}
function http_post(url: pointer, body: pointer) -> int {
let h = http_open("POST", url)
if h != 0 { http_body(h, body); http_send_req(h) }
return h
}
function http_request(method: pointer, url: pointer, body: pointer) -> int {
let h = http_open(method, url)
if h == 0 { return 0 }
if http_cstr_len(body) > 0 { http_body(h, body) }
http_send_req(h)
return h
}
# ---- polling + response ----------------------------------------------------
# -1 while pending, 0 on a transport error, else the HTTP status code.
function http_poll(h: int) -> int {
if not http_valid(h) { return 0 }
let s = h - 1
if h_native[s] == 0 { return h_status[s] } # a parsed handle is ready immediately
if h_sent[s] == 0 { return 0 - 1 }
if h_res[s] == 1 { return h_status[s] }
if hs_done(s) == 0 { return 0 - 1 }
# first time we see it finished: cache the fields off the worker.
h_status[s] = hs_status(s)
h_body[s] = hs_body(s)
h_blen[s] = hs_blen(s)
h_res[s] = 1
return h_status[s]
}
function http_status_of(h: int) -> int {
if not http_valid(h) { return 0 }
return h_status[h - 1]
}
function http_ok(h: int) -> bool {
let st = http_status_of(h)
return (st >= 200) and (st < 300)
}
function http_text(h: int) -> pointer {
if not http_valid(h) { return null }
return h_body[h - 1]
}
function http_body_len(h: int) -> int {
if not http_valid(h) { return 0 }
return h_blen[h - 1]
}
function http_header_of(h: int, name: pointer) -> pointer {
if not http_valid(h) { return null }
let s = h - 1
if h_native[s] == 1 { return hs_header(s, name) }
return http_find_header(h_hdr[s], name)
}
function http_close(h: int) -> void {
if not http_valid(h) { return }
let s = h - 1
if h_native[s] == 1 { hs_free(s) }
else {
if h_body[s] != null { free(h_body[s]) }
if h_hdr[s] != null { free(h_hdr[s]) }
}
h_used[s] = 0
h_req[s] = null; h_body[s] = null; h_hdr[s] = null
}
# ---- the pure-Ludic response parser (transport-independent) -----------------
# Copy [a, b) of src into a fresh NUL-terminated buffer.
function http_slice_dup(src: pointer, a: int, b: int) -> pointer {
var n = b - a
if n < 0 { n = 0 }
let out = bytes(n + 1)
var i = 0
while i < n { out[i] = src[a + i]; i = i + 1 }
out[n] = 0
return out
}
function http_is_digit(c: int) -> bool { return (c >= 48) and (c <= 57) }
function http_lower(c: int) -> int { if (c >= 65) and (c <= 90) { return c + 32 }; return c }
# Parse a raw HTTP/1.1 response (status line + headers + blank line + body) into
# a ready handle. Useful for cached/custom transports and offline tests.
function http_parse(resp: pointer, len: int) -> int {
let s = http_slot_alloc()
if s < 0 { return 0 }
h_native[s] = 0
# status: the token after the first space on the status line.
var i = 0
while (i < len) and (resp[i] != 32) { i = i + 1 } # skip "HTTP/1.1"
while (i < len) and (resp[i] == 32) { i = i + 1 } # skip the space(s)
var st = 0
while (i < len) and http_is_digit(resp[i]) { st = (st * 10) + (resp[i] - 48); i = i + 1 }
h_status[s] = st
# header/body split at the first CRLFCRLF.
var split = 0 - 1
var j = 0
while (j + 3) < len {
if (resp[j] == 13) and (resp[j + 1] == 10) and (resp[j + 2] == 13) and (resp[j + 3] == 10) {
split = j
j = len
} else { j = j + 1 }
}
var hdr_end = split
var body_start = split + 4
if split < 0 { hdr_end = len; body_start = len } # headers only, no body
# header block starts after the status line's CRLF.
var hs = 0
while (hs + 1) < len {
if (resp[hs] == 13) and (resp[hs + 1] == 10) { hs = hs + 2; break }
hs = hs + 1
}
if hs > hdr_end { hs = hdr_end }
h_hdr[s] = http_slice_dup(resp, hs, hdr_end)
h_body[s] = http_slice_dup(resp, body_start, len)
h_blen[s] = len - body_start
if h_blen[s] < 0 { h_blen[s] = 0 }
return s + 1
}
# Look up a header value in a raw header block (case-insensitive), returning a
# fresh NUL-terminated copy, or null if absent. Lines are "Name: value\r\n".
function http_find_header(hdr: pointer, name: pointer) -> pointer {
if hdr == null { return null }
let nlen = http_cstr_len(name)
let hlen = http_cstr_len(hdr)
var i = 0
while i < hlen {
# match name at the start of a line, followed by ':'.
var k = 0
while (k < nlen) and ((i + k) < hlen) and (http_lower(hdr[i + k]) == http_lower(name[k])) { k = k + 1 }
if (k == nlen) and ((i + k) < hlen) and (hdr[i + k] == 58) {
var v = i + k + 1
while (v < hlen) and (hdr[v] == 32) { v = v + 1 } # skip spaces after ':'
var e = v
while (e < hlen) and (hdr[e] != 13) and (hdr[e] != 10) { e = e + 1 }
return http_slice_dup(hdr, v, e)
}
# advance to the next line.
while (i < hlen) and (hdr[i] != 10) { i = i + 1 }
i = i + 1
}
return null
}