mt-lang 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/index.html +4 -3
- data/lib/milk_tea/base.rb +1 -1
- data/std/asset_pack.mt +267 -0
- data/std/async/libuv_runtime.mt +539 -0
- data/std/async/mailbox.mt +191 -0
- data/std/async/runtime.mt +87 -0
- data/std/async.mt +87 -0
- data/std/base64.mt +185 -0
- data/std/behavior_tree.mt +396 -0
- data/std/binary.mt +314 -0
- data/std/binary_heap.mt +157 -0
- data/std/bitset.mt +239 -0
- data/std/box2d.mt +535 -0
- data/std/bytes.mt +46 -0
- data/std/c/box2d.mt +1254 -0
- data/std/c/cgltf.mt +647 -0
- data/std/c/cjson.mt +106 -0
- data/std/c/crypto.mt +11 -0
- data/std/c/crypto_support.h +51 -0
- data/std/c/ctype.mt +19 -0
- data/std/c/ctype_bindgen.h +29 -0
- data/std/c/curl.mt +1223 -0
- data/std/c/enet.mt +473 -0
- data/std/c/errno.mt +41 -0
- data/std/c/errno_bindgen.h +53 -0
- data/std/c/flecs.mt +2945 -0
- data/std/c/fs.linux.mt +43 -0
- data/std/c/fs.windows.mt +43 -0
- data/std/c/fs_support.h +954 -0
- data/std/c/gl.mt +2065 -0
- data/std/c/gl_registry_helpers.h +10748 -0
- data/std/c/glfw.mt +499 -0
- data/std/c/libc.mt +167 -0
- data/std/c/libuv.mt +1491 -0
- data/std/c/math.mt +22 -0
- data/std/c/math_bindgen.h +31 -0
- data/std/c/miniaudio.mt +3420 -0
- data/std/c/pcre2.mt +735 -0
- data/std/c/process.mt +58 -0
- data/std/c/process_support.h +1147 -0
- data/std/c/raygui.mt +1602 -0
- data/std/c/raylib.mt +1245 -0
- data/std/c/raymath.mt +163 -0
- data/std/c/rlgl.mt +434 -0
- data/std/c/rpng.mt +60 -0
- data/std/c/rres.mt +187 -0
- data/std/c/sdl3.mt +4132 -0
- data/std/c/sqlite3.mt +951 -0
- data/std/c/stb_image.mt +52 -0
- data/std/c/stb_image_resize2.mt +128 -0
- data/std/c/stb_image_write.mt +19 -0
- data/std/c/stb_rect_pack.mt +38 -0
- data/std/c/stb_truetype.mt +175 -0
- data/std/c/stb_vorbis.mt +31 -0
- data/std/c/stdio.mt +90 -0
- data/std/c/steamworks.h +5769 -0
- data/std/c/steamworks.mt +4771 -0
- data/std/c/string.mt +16 -0
- data/std/c/string_bindgen.h +36 -0
- data/std/c/sync.mt +33 -0
- data/std/c/sync_support.h +219 -0
- data/std/c/terminal.mt +24 -0
- data/std/c/terminal_support.h +408 -0
- data/std/c/time.mt +26 -0
- data/std/c/tls.mt +24 -0
- data/std/c/tls_support.h +626 -0
- data/std/c/tracy.mt +53 -0
- data/std/c/zlib.mt +20 -0
- data/std/c/zlib_support.h +239 -0
- data/std/c/zstd.mt +231 -0
- data/std/cell.mt +60 -0
- data/std/cgltf.mt +115 -0
- data/std/cjson.mt +90 -0
- data/std/cli.mt +768 -0
- data/std/cookie.mt +198 -0
- data/std/counter.mt +160 -0
- data/std/crypto.mt +74 -0
- data/std/cstring.mt +13 -0
- data/std/ctype.mt +52 -0
- data/std/curl/runtime.mt +261 -0
- data/std/curl.mt +38 -0
- data/std/deque.mt +348 -0
- data/std/encoding.mt +93 -0
- data/std/enet.mt +133 -0
- data/std/env.mt +41 -0
- data/std/errno.mt +47 -0
- data/std/flecs.mt +978 -0
- data/std/fmt.mt +271 -0
- data/std/fs.linux.mt +696 -0
- data/std/fs.windows.mt +711 -0
- data/std/fsm.mt +267 -0
- data/std/gl.mt +2062 -0
- data/std/glfw.mt +471 -0
- data/std/goap.mt +342 -0
- data/std/graph.mt +521 -0
- data/std/gzip.mt +77 -0
- data/std/hash.mt +429 -0
- data/std/http/server.mt +810 -0
- data/std/http.mt +1227 -0
- data/std/intern.mt +53 -0
- data/std/jobs.mt +288 -0
- data/std/json.mt +627 -0
- data/std/libc.mt +34 -0
- data/std/libuv.mt +554 -0
- data/std/linear_algebra.mt +243 -0
- data/std/linked_map.mt +372 -0
- data/std/linked_map_view.mt +49 -0
- data/std/linked_set.mt +117 -0
- data/std/log.mt +69 -0
- data/std/map.mt +418 -0
- data/std/math.mt +24 -0
- data/std/mem/arena.mt +137 -0
- data/std/mem/endian.mt +51 -0
- data/std/mem/heap.mt +298 -0
- data/std/mem/pool.mt +164 -0
- data/std/mem/stack.mt +47 -0
- data/std/mem/tracking.mt +119 -0
- data/std/miniaudio.mt +1299 -0
- data/std/multiset.mt +191 -0
- data/std/net/channel.mt +727 -0
- data/std/net/clock.mt +237 -0
- data/std/net/discovery.mt +277 -0
- data/std/net/lobby.mt +813 -0
- data/std/net/manager.mt +539 -0
- data/std/net/mux.mt +839 -0
- data/std/net/nat.mt +122 -0
- data/std/net/packet.mt +219 -0
- data/std/net/punch.mt +162 -0
- data/std/net/rpc.mt +119 -0
- data/std/net/session.mt +1157 -0
- data/std/net/stun.mt +252 -0
- data/std/net/sync.mt +183 -0
- data/std/net/turn.mt +459 -0
- data/std/net.mt +2944 -0
- data/std/oauth2.mt +408 -0
- data/std/option.mt +51 -0
- data/std/ordered_map.mt +497 -0
- data/std/ordered_set.mt +348 -0
- data/std/path.mt +368 -0
- data/std/pcre2/runtime.mt +35 -0
- data/std/pcre2.mt +78 -0
- data/std/priority_queue.mt +55 -0
- data/std/process.mt +837 -0
- data/std/queue.mt +55 -0
- data/std/random.mt +146 -0
- data/std/raygui.mt +99 -0
- data/std/raylib/debug_console.mt +71 -0
- data/std/raylib/easing.mt +388 -0
- data/std/raylib/packed_assets.mt +255 -0
- data/std/raylib/runtime.mt +30 -0
- data/std/raylib/tracy_gpu.mt +37 -0
- data/std/raylib.mt +1510 -0
- data/std/raymath.mt +153 -0
- data/std/result.mt +89 -0
- data/std/rlgl.mt +268 -0
- data/std/rpng.mt +46 -0
- data/std/rres.mt +36 -0
- data/std/sdl3/runtime.mt +54 -0
- data/std/sdl3.mt +1731 -0
- data/std/serialize.mt +68 -0
- data/std/set.mt +124 -0
- data/std/spatial.mt +176 -0
- data/std/sqlite3.mt +151 -0
- data/std/stack.mt +55 -0
- data/std/stb_image.mt +47 -0
- data/std/stb_image_resize2.mt +41 -0
- data/std/stb_image_write.mt +17 -0
- data/std/stb_rect_pack.mt +15 -0
- data/std/stb_truetype.mt +77 -0
- data/std/stb_vorbis.mt +16 -0
- data/std/stdio.mt +88 -0
- data/std/steamworks.mt +1542 -0
- data/std/str.mt +293 -0
- data/std/string.mt +234 -0
- data/std/sync.mt +194 -0
- data/std/tar.mt +704 -0
- data/std/terminal.mt +1002 -0
- data/std/testing.mt +266 -0
- data/std/thread.mt +120 -0
- data/std/time.mt +105 -0
- data/std/tls.mt +616 -0
- data/std/toml.mt +1310 -0
- data/std/tracy.mt +42 -0
- data/std/uri.mt +118 -0
- data/std/url.mt +372 -0
- data/std/vec.mt +433 -0
- data/std/zstd.mt +94 -0
- metadata +187 -2
data/std/http.mt
ADDED
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
import std.bytes as bytes
|
|
2
|
+
import std.fmt as fmt
|
|
3
|
+
import std.mem.heap as heap
|
|
4
|
+
import std.net as net
|
|
5
|
+
import std.str as text
|
|
6
|
+
import std.string as string
|
|
7
|
+
import std.tls as tls
|
|
8
|
+
import std.vec as vec
|
|
9
|
+
|
|
10
|
+
public struct Error:
|
|
11
|
+
message: string.String
|
|
12
|
+
|
|
13
|
+
public struct Header:
|
|
14
|
+
name: string.String
|
|
15
|
+
value: string.String
|
|
16
|
+
|
|
17
|
+
public struct RequestHeader:
|
|
18
|
+
name: str
|
|
19
|
+
value: str
|
|
20
|
+
|
|
21
|
+
public struct Response:
|
|
22
|
+
status_code: int
|
|
23
|
+
reason: string.String
|
|
24
|
+
headers: vec.Vec[Header]
|
|
25
|
+
body: bytes.Bytes
|
|
26
|
+
|
|
27
|
+
enum UrlScheme: int
|
|
28
|
+
http = 1
|
|
29
|
+
https = 2
|
|
30
|
+
|
|
31
|
+
struct ParsedUrl:
|
|
32
|
+
scheme: UrlScheme
|
|
33
|
+
host: string.String
|
|
34
|
+
authority: string.String
|
|
35
|
+
target: string.String
|
|
36
|
+
port: int
|
|
37
|
+
|
|
38
|
+
struct ResponseHead:
|
|
39
|
+
status_code: int
|
|
40
|
+
reason: string.String
|
|
41
|
+
headers: vec.Vec[Header]
|
|
42
|
+
content_length: Option[ptr_uint]
|
|
43
|
+
chunked: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
function status_error(message: str) -> Error:
|
|
47
|
+
return Error(message = string.String.from_str(message))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
function status_net_error(net_error: net.Error) -> Error:
|
|
51
|
+
var owned_error = net_error
|
|
52
|
+
let message = string.String.from_str(owned_error.message.as_str())
|
|
53
|
+
owned_error.release()
|
|
54
|
+
return Error(message = message)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function status_tls_error(tls_error: tls.Error) -> Error:
|
|
58
|
+
var owned_error = tls_error
|
|
59
|
+
let message = string.String.from_str(owned_error.message.as_str())
|
|
60
|
+
owned_error.release()
|
|
61
|
+
return Error(message = message)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
function url_error(detail: str) -> Error:
|
|
65
|
+
return status_error(f"invalid http url: #{detail}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
function response_error(detail: str) -> Error:
|
|
69
|
+
return status_error(f"invalid http response: #{detail}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
function transport_error(detail: str) -> Error:
|
|
73
|
+
return status_error(detail)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
function ascii_fold(value: ubyte) -> ubyte:
|
|
77
|
+
if value >= 65 and value <= 90:
|
|
78
|
+
return value + 32
|
|
79
|
+
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
function ascii_case_equal(left: str, right: str) -> bool:
|
|
84
|
+
if left.len != right.len:
|
|
85
|
+
return false
|
|
86
|
+
|
|
87
|
+
var index: ptr_uint = 0
|
|
88
|
+
while index < left.len:
|
|
89
|
+
if ascii_fold(left.byte_at(index)) != ascii_fold(right.byte_at(index)):
|
|
90
|
+
return false
|
|
91
|
+
index += 1
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
function find_byte_from(text_value: str, value: ubyte, start: ptr_uint) -> Option[ptr_uint]:
|
|
97
|
+
var index = start
|
|
98
|
+
while index < text_value.len:
|
|
99
|
+
if text_value.byte_at(index) == value:
|
|
100
|
+
return Option[ptr_uint].some(value = index)
|
|
101
|
+
index += 1
|
|
102
|
+
|
|
103
|
+
return Option[ptr_uint].none
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function count_byte(text_value: str, value: ubyte) -> ptr_uint:
|
|
107
|
+
var count: ptr_uint = 0
|
|
108
|
+
var index: ptr_uint = 0
|
|
109
|
+
while index < text_value.len:
|
|
110
|
+
if text_value.byte_at(index) == value:
|
|
111
|
+
count += 1
|
|
112
|
+
index += 1
|
|
113
|
+
|
|
114
|
+
return count
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
function parse_decimal(text_value: str) -> Option[ptr_uint]:
|
|
118
|
+
if text_value.len == 0:
|
|
119
|
+
return Option[ptr_uint].none
|
|
120
|
+
|
|
121
|
+
var value: ptr_uint = 0
|
|
122
|
+
var index: ptr_uint = 0
|
|
123
|
+
while index < text_value.len:
|
|
124
|
+
let current = text_value.byte_at(index)
|
|
125
|
+
if current < 48 or current > 57:
|
|
126
|
+
return Option[ptr_uint].none
|
|
127
|
+
|
|
128
|
+
let digit = ptr_uint<-(current - 48ub)
|
|
129
|
+
if value > (heap.ptr_uint_max - digit) / 10:
|
|
130
|
+
return Option[ptr_uint].none
|
|
131
|
+
|
|
132
|
+
value = value * 10 + digit
|
|
133
|
+
index += 1
|
|
134
|
+
|
|
135
|
+
return Option[ptr_uint].some(value = value)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
function parse_port(text_value: str) -> Option[int]:
|
|
139
|
+
let parsed = parse_decimal(text_value) else:
|
|
140
|
+
return Option[int].none
|
|
141
|
+
|
|
142
|
+
if parsed > 65535:
|
|
143
|
+
return Option[int].none
|
|
144
|
+
|
|
145
|
+
return Option[int].some(value = int<-parsed)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
function parse_target(rest: str) -> Result[string.String, Error]:
|
|
149
|
+
if rest.len == 0:
|
|
150
|
+
return Result[string.String, Error].success(value = string.String.from_str("/"))
|
|
151
|
+
|
|
152
|
+
let fragment_index = find_byte_from(rest, 35, 0)
|
|
153
|
+
var target_text = rest
|
|
154
|
+
match fragment_index:
|
|
155
|
+
Option.none:
|
|
156
|
+
pass
|
|
157
|
+
Option.some as payload:
|
|
158
|
+
target_text = rest.slice(0, payload.value)
|
|
159
|
+
|
|
160
|
+
if target_text.len == 0:
|
|
161
|
+
return Result[string.String, Error].success(value = string.String.from_str("/"))
|
|
162
|
+
|
|
163
|
+
let first = target_text.byte_at(0)
|
|
164
|
+
if first == 47:
|
|
165
|
+
return Result[string.String, Error].success(value = string.String.from_str(target_text))
|
|
166
|
+
|
|
167
|
+
if first == 63:
|
|
168
|
+
var target = string.String.from_str("/")
|
|
169
|
+
target.append(target_text)
|
|
170
|
+
return Result[string.String, Error].success(value = target)
|
|
171
|
+
|
|
172
|
+
return Result[string.String, Error].failure(error = url_error("path must start with '/' or '?'") )
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
function default_port_for_scheme(scheme: UrlScheme) -> int:
|
|
176
|
+
match scheme:
|
|
177
|
+
UrlScheme.http:
|
|
178
|
+
return 80
|
|
179
|
+
UrlScheme.https:
|
|
180
|
+
return 443
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
function parse_url(url: str) -> Result[ParsedUrl, Error]:
|
|
184
|
+
var scheme = UrlScheme.http
|
|
185
|
+
var remainder = url
|
|
186
|
+
if url.starts_with("http://"):
|
|
187
|
+
remainder = url.slice(7, url.len - 7)
|
|
188
|
+
else if url.starts_with("https://"):
|
|
189
|
+
scheme = UrlScheme.https
|
|
190
|
+
remainder = url.slice(8, url.len - 8)
|
|
191
|
+
else:
|
|
192
|
+
return Result[ParsedUrl, Error].failure(error = url_error("URL must start with http:// or https://"))
|
|
193
|
+
|
|
194
|
+
if remainder.len == 0:
|
|
195
|
+
return Result[ParsedUrl, Error].failure(error = url_error("missing authority"))
|
|
196
|
+
|
|
197
|
+
var authority_end = remainder.len
|
|
198
|
+
var index: ptr_uint = 0
|
|
199
|
+
while index < remainder.len:
|
|
200
|
+
let current = remainder.byte_at(index)
|
|
201
|
+
if current == 47 or current == 63 or current == 35:
|
|
202
|
+
authority_end = index
|
|
203
|
+
break
|
|
204
|
+
index += 1
|
|
205
|
+
|
|
206
|
+
let authority_text = remainder.slice(0, authority_end)
|
|
207
|
+
if authority_text.len == 0:
|
|
208
|
+
return Result[ParsedUrl, Error].failure(error = url_error("missing authority"))
|
|
209
|
+
|
|
210
|
+
if count_byte(authority_text, 64) != 0:
|
|
211
|
+
return Result[ParsedUrl, Error].failure(error = url_error("userinfo is not supported"))
|
|
212
|
+
|
|
213
|
+
var host_text = authority_text
|
|
214
|
+
var port = default_port_for_scheme(scheme)
|
|
215
|
+
if authority_text.byte_at(0) == 91:
|
|
216
|
+
var closing_bracket: ptr_uint = 0
|
|
217
|
+
match find_byte_from(authority_text, 93, 1):
|
|
218
|
+
Option.none:
|
|
219
|
+
return Result[ParsedUrl, Error].failure(error = url_error("missing closing ']' for IPv6 host"))
|
|
220
|
+
Option.some as payload:
|
|
221
|
+
closing_bracket = payload.value
|
|
222
|
+
|
|
223
|
+
if closing_bracket == 1:
|
|
224
|
+
return Result[ParsedUrl, Error].failure(error = url_error("host is empty"))
|
|
225
|
+
|
|
226
|
+
host_text = authority_text.slice(1, closing_bracket - 1)
|
|
227
|
+
if closing_bracket + 1 < authority_text.len:
|
|
228
|
+
if authority_text.byte_at(closing_bracket + 1) != 58:
|
|
229
|
+
return Result[ParsedUrl, Error].failure(error = url_error("unexpected text after IPv6 host"))
|
|
230
|
+
|
|
231
|
+
let port_start = closing_bracket + 2
|
|
232
|
+
let port_text = authority_text.slice(port_start, authority_text.len - port_start)
|
|
233
|
+
match parse_port(port_text):
|
|
234
|
+
Option.none:
|
|
235
|
+
return Result[ParsedUrl, Error].failure(error = url_error("invalid port"))
|
|
236
|
+
Option.some as payload:
|
|
237
|
+
port = payload.value
|
|
238
|
+
else:
|
|
239
|
+
let colon_count = count_byte(authority_text, 58)
|
|
240
|
+
if colon_count > 1:
|
|
241
|
+
return Result[ParsedUrl, Error].failure(error = url_error("IPv6 hosts must be wrapped in []"))
|
|
242
|
+
|
|
243
|
+
if colon_count == 1:
|
|
244
|
+
var colon_index: ptr_uint = 0
|
|
245
|
+
match authority_text.find_byte(58):
|
|
246
|
+
Option.none:
|
|
247
|
+
fatal(c"http.parse_url missing authority port separator")
|
|
248
|
+
Option.some as payload:
|
|
249
|
+
colon_index = payload.value
|
|
250
|
+
|
|
251
|
+
if colon_index == 0:
|
|
252
|
+
return Result[ParsedUrl, Error].failure(error = url_error("host is empty"))
|
|
253
|
+
|
|
254
|
+
host_text = authority_text.slice(0, colon_index)
|
|
255
|
+
let port_start = colon_index + 1
|
|
256
|
+
let port_text = authority_text.slice(port_start, authority_text.len - port_start)
|
|
257
|
+
match parse_port(port_text):
|
|
258
|
+
Option.none:
|
|
259
|
+
return Result[ParsedUrl, Error].failure(error = url_error("invalid port"))
|
|
260
|
+
Option.some as payload:
|
|
261
|
+
port = payload.value
|
|
262
|
+
|
|
263
|
+
if host_text.len == 0:
|
|
264
|
+
return Result[ParsedUrl, Error].failure(error = url_error("host is empty"))
|
|
265
|
+
|
|
266
|
+
let rest = remainder.slice(authority_end, remainder.len - authority_end)
|
|
267
|
+
let target_result = parse_target(rest)
|
|
268
|
+
match target_result:
|
|
269
|
+
Result.failure as payload:
|
|
270
|
+
return Result[ParsedUrl, Error].failure(error = payload.error)
|
|
271
|
+
Result.success as payload:
|
|
272
|
+
let target = payload.value
|
|
273
|
+
return Result[ParsedUrl, Error].success(
|
|
274
|
+
value = ParsedUrl(
|
|
275
|
+
scheme = scheme,
|
|
276
|
+
host = string.String.from_str(host_text),
|
|
277
|
+
authority = string.String.from_str(authority_text),
|
|
278
|
+
target = target,
|
|
279
|
+
port = port
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
function append_request_text(output: ref[vec.Vec[ubyte]], value: str) -> void:
|
|
285
|
+
output.append_span(text.as_byte_span(value))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
function append_request_header_line(output: ref[vec.Vec[ubyte]], name: str, value: str) -> void:
|
|
289
|
+
append_request_text(output, name)
|
|
290
|
+
append_request_text(output, ": ")
|
|
291
|
+
append_request_text(output, value)
|
|
292
|
+
append_request_text(output, "\r\n")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
function valid_http_token(value: str) -> bool:
|
|
296
|
+
if value.len == 0:
|
|
297
|
+
return false
|
|
298
|
+
|
|
299
|
+
var index: ptr_uint = 0
|
|
300
|
+
while index < value.len:
|
|
301
|
+
let current = value.byte_at(index)
|
|
302
|
+
if current <= 32 or current >= 127 or current == 58:
|
|
303
|
+
return false
|
|
304
|
+
index += 1
|
|
305
|
+
|
|
306
|
+
return true
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
function valid_http_header_value(value: str) -> bool:
|
|
310
|
+
var index: ptr_uint = 0
|
|
311
|
+
while index < value.len:
|
|
312
|
+
let current = value.byte_at(index)
|
|
313
|
+
if current == 13 or current == 10:
|
|
314
|
+
return false
|
|
315
|
+
index += 1
|
|
316
|
+
|
|
317
|
+
return true
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
function build_request(
|
|
321
|
+
url: ParsedUrl,
|
|
322
|
+
method: str,
|
|
323
|
+
headers: span[RequestHeader],
|
|
324
|
+
body: Option[span[ubyte]]
|
|
325
|
+
) -> Result[vec.Vec[ubyte], Error]:
|
|
326
|
+
if not valid_http_token(method):
|
|
327
|
+
return Result[vec.Vec[ubyte], Error].failure(error = status_error("invalid http method"))
|
|
328
|
+
|
|
329
|
+
var body_length: ptr_uint = 0
|
|
330
|
+
match body:
|
|
331
|
+
Option.none:
|
|
332
|
+
pass
|
|
333
|
+
Option.some as payload:
|
|
334
|
+
body_length = payload.value.len
|
|
335
|
+
|
|
336
|
+
var request = vec.Vec[ubyte].with_capacity(url.target.len + url.authority.len + method.len + body_length + 128)
|
|
337
|
+
|
|
338
|
+
var has_host = false
|
|
339
|
+
var has_connection = false
|
|
340
|
+
var has_user_agent = false
|
|
341
|
+
var content_length_seen = false
|
|
342
|
+
|
|
343
|
+
var index: ptr_uint = 0
|
|
344
|
+
while index < headers.len:
|
|
345
|
+
let header = unsafe: read(headers.data + index)
|
|
346
|
+
if not valid_http_token(header.name):
|
|
347
|
+
request.release()
|
|
348
|
+
return Result[vec.Vec[ubyte], Error].failure(error = status_error("request header name is invalid"))
|
|
349
|
+
|
|
350
|
+
if not valid_http_header_value(header.value):
|
|
351
|
+
request.release()
|
|
352
|
+
return Result[vec.Vec[ubyte], Error].failure(error = status_error("request header value is invalid"))
|
|
353
|
+
|
|
354
|
+
if ascii_case_equal(header.name, "Host"):
|
|
355
|
+
has_host = true
|
|
356
|
+
else if ascii_case_equal(header.name, "Connection"):
|
|
357
|
+
has_connection = true
|
|
358
|
+
else if ascii_case_equal(header.name, "User-Agent"):
|
|
359
|
+
has_user_agent = true
|
|
360
|
+
else if ascii_case_equal(header.name, "Content-Length"):
|
|
361
|
+
if content_length_seen:
|
|
362
|
+
request.release()
|
|
363
|
+
return Result[
|
|
364
|
+
vec.Vec[ubyte],
|
|
365
|
+
Error
|
|
366
|
+
].failure(error = status_error("request Content-Length must not be repeated"))
|
|
367
|
+
|
|
368
|
+
let content_length = parse_decimal(header.value.trim_ascii_whitespace()) else:
|
|
369
|
+
request.release()
|
|
370
|
+
return Result[
|
|
371
|
+
vec.Vec[ubyte],
|
|
372
|
+
Error
|
|
373
|
+
].failure(error = status_error("request Content-Length must be a decimal integer"))
|
|
374
|
+
|
|
375
|
+
if content_length != body_length:
|
|
376
|
+
request.release()
|
|
377
|
+
return Result[
|
|
378
|
+
vec.Vec[ubyte],
|
|
379
|
+
Error
|
|
380
|
+
].failure(error = status_error("request Content-Length does not match body length"))
|
|
381
|
+
|
|
382
|
+
content_length_seen = true
|
|
383
|
+
|
|
384
|
+
index += 1
|
|
385
|
+
|
|
386
|
+
append_request_text(ref_of(request), method)
|
|
387
|
+
append_request_text(ref_of(request), " ")
|
|
388
|
+
append_request_text(ref_of(request), url.target.as_str())
|
|
389
|
+
append_request_text(ref_of(request), " HTTP/1.1\r\n")
|
|
390
|
+
|
|
391
|
+
if not has_host:
|
|
392
|
+
append_request_header_line(ref_of(request), "Host", url.authority.as_str())
|
|
393
|
+
|
|
394
|
+
if not has_connection:
|
|
395
|
+
append_request_header_line(ref_of(request), "Connection", "close")
|
|
396
|
+
|
|
397
|
+
if not has_user_agent:
|
|
398
|
+
append_request_header_line(ref_of(request), "User-Agent", "milk-tea/std.http")
|
|
399
|
+
|
|
400
|
+
if not content_length_seen:
|
|
401
|
+
var content_length = fmt.to_string_ptr_uint(body_length)
|
|
402
|
+
defer content_length.release()
|
|
403
|
+
append_request_header_line(ref_of(request), "Content-Length", content_length.as_str())
|
|
404
|
+
|
|
405
|
+
index = 0
|
|
406
|
+
while index < headers.len:
|
|
407
|
+
let header = unsafe: read(headers.data + index)
|
|
408
|
+
append_request_header_line(ref_of(request), header.name, header.value)
|
|
409
|
+
index += 1
|
|
410
|
+
|
|
411
|
+
append_request_text(ref_of(request), "\r\n")
|
|
412
|
+
|
|
413
|
+
match body:
|
|
414
|
+
Option.none:
|
|
415
|
+
pass
|
|
416
|
+
Option.some as payload:
|
|
417
|
+
request.append_span(payload.value)
|
|
418
|
+
|
|
419
|
+
return Result[vec.Vec[ubyte], Error].success(value = request)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
function find_crlf(text_value: str, start: ptr_uint) -> Option[ptr_uint]:
|
|
423
|
+
if text_value.len < 2 or start + 1 >= text_value.len:
|
|
424
|
+
return Option[ptr_uint].none
|
|
425
|
+
|
|
426
|
+
var index = start
|
|
427
|
+
while index + 1 < text_value.len:
|
|
428
|
+
if text_value.byte_at(index) == 13 and text_value.byte_at(index + 1) == 10:
|
|
429
|
+
return Option[ptr_uint].some(value = index)
|
|
430
|
+
index += 1
|
|
431
|
+
|
|
432
|
+
return Option[ptr_uint].none
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
function find_header_terminator(data: span[ubyte]) -> Option[ptr_uint]:
|
|
436
|
+
if data.len < 4:
|
|
437
|
+
return Option[ptr_uint].none
|
|
438
|
+
|
|
439
|
+
var index: ptr_uint = 0
|
|
440
|
+
while index + 3 < data.len:
|
|
441
|
+
if (
|
|
442
|
+
unsafe: read(data.data + index) == 13
|
|
443
|
+
and read(data.data + index + 1) == 10
|
|
444
|
+
and read(data.data + index + 2) == 13
|
|
445
|
+
and read(data.data + index + 3) == 10
|
|
446
|
+
):
|
|
447
|
+
return Option[ptr_uint].some(value = index)
|
|
448
|
+
index += 1
|
|
449
|
+
|
|
450
|
+
return Option[ptr_uint].none
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
function find_crlf_bytes(data: span[ubyte], start: ptr_uint) -> Option[ptr_uint]:
|
|
454
|
+
if data.len < 2 or start + 1 >= data.len:
|
|
455
|
+
return Option[ptr_uint].none
|
|
456
|
+
|
|
457
|
+
var index = start
|
|
458
|
+
while index + 1 < data.len:
|
|
459
|
+
if unsafe: read(data.data + index) == 13 and read(data.data + index + 1) == 10:
|
|
460
|
+
return Option[ptr_uint].some(value = index)
|
|
461
|
+
index += 1
|
|
462
|
+
|
|
463
|
+
return Option[ptr_uint].none
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
function hexadecimal_digit_value(current: ubyte) -> Option[ptr_uint]:
|
|
467
|
+
if current < 48:
|
|
468
|
+
return Option[ptr_uint].none
|
|
469
|
+
|
|
470
|
+
if current <= 57:
|
|
471
|
+
return Option[ptr_uint].some(value = ptr_uint<-(current - 48ub))
|
|
472
|
+
|
|
473
|
+
if current < 65:
|
|
474
|
+
return Option[ptr_uint].none
|
|
475
|
+
|
|
476
|
+
if current <= 70:
|
|
477
|
+
return Option[ptr_uint].some(value = ptr_uint<-(current - 55ub))
|
|
478
|
+
|
|
479
|
+
if current < 97:
|
|
480
|
+
return Option[ptr_uint].none
|
|
481
|
+
|
|
482
|
+
if current <= 102:
|
|
483
|
+
return Option[ptr_uint].some(value = ptr_uint<-(current - 87ub))
|
|
484
|
+
|
|
485
|
+
return Option[ptr_uint].none
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
function parse_hexadecimal(text_value: str) -> Option[ptr_uint]:
|
|
489
|
+
if text_value.len == 0:
|
|
490
|
+
return Option[ptr_uint].none
|
|
491
|
+
|
|
492
|
+
var value: ptr_uint = 0
|
|
493
|
+
var index: ptr_uint = 0
|
|
494
|
+
while index < text_value.len:
|
|
495
|
+
let current = text_value.byte_at(index)
|
|
496
|
+
let digit = hexadecimal_digit_value(current) else:
|
|
497
|
+
return Option[ptr_uint].none
|
|
498
|
+
|
|
499
|
+
if value > (heap.ptr_uint_max - digit) / 16:
|
|
500
|
+
return Option[ptr_uint].none
|
|
501
|
+
|
|
502
|
+
value = value * 16 + digit
|
|
503
|
+
index += 1
|
|
504
|
+
|
|
505
|
+
return Option[ptr_uint].some(value = value)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
function parse_chunk_size(text_value: str) -> Option[ptr_uint]:
|
|
509
|
+
var size_text = text_value
|
|
510
|
+
let extension = find_byte_from(text_value, 59, 0) else:
|
|
511
|
+
let parsed = parse_hexadecimal(size_text.trim_ascii_whitespace()) else:
|
|
512
|
+
return Option[ptr_uint].none
|
|
513
|
+
return Option[ptr_uint].some(value = parsed)
|
|
514
|
+
|
|
515
|
+
size_text = text_value.slice(0, extension)
|
|
516
|
+
let parsed = parse_hexadecimal(size_text.trim_ascii_whitespace()) else:
|
|
517
|
+
return Option[ptr_uint].none
|
|
518
|
+
return Option[ptr_uint].some(value = parsed)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
function discard_buffer_prefix(buffer: ref[vec.Vec[ubyte]], count: ptr_uint) -> void:
|
|
522
|
+
if count == 0:
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
if count >= buffer.len:
|
|
526
|
+
buffer.clear()
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
let data = buffer.data else:
|
|
530
|
+
fatal(c"http.discard_buffer_prefix missing storage")
|
|
531
|
+
|
|
532
|
+
let remaining = buffer.len - count
|
|
533
|
+
unsafe:
|
|
534
|
+
let data_ptr = data
|
|
535
|
+
var index: ptr_uint = 0
|
|
536
|
+
while index < remaining:
|
|
537
|
+
read(data_ptr + index) = read(data_ptr + count + index)
|
|
538
|
+
index += 1
|
|
539
|
+
|
|
540
|
+
buffer.len = remaining
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
function parse_response_head(header_text: str) -> Result[ResponseHead, Error]:
|
|
544
|
+
var line_end = header_text.len
|
|
545
|
+
var header_index = header_text.len
|
|
546
|
+
match find_crlf(header_text, 0):
|
|
547
|
+
Option.none:
|
|
548
|
+
pass
|
|
549
|
+
Option.some as payload:
|
|
550
|
+
line_end = payload.value
|
|
551
|
+
header_index = line_end + 2
|
|
552
|
+
|
|
553
|
+
let status_line = header_text.slice(0, line_end)
|
|
554
|
+
if not status_line.starts_with("HTTP/1."):
|
|
555
|
+
return Result[ResponseHead, Error].failure(error = response_error("unsupported HTTP version"))
|
|
556
|
+
|
|
557
|
+
var first_space: ptr_uint = 0
|
|
558
|
+
match status_line.find_byte(32):
|
|
559
|
+
Option.none:
|
|
560
|
+
return Result[ResponseHead, Error].failure(error = response_error("missing status code"))
|
|
561
|
+
Option.some as payload:
|
|
562
|
+
first_space = payload.value
|
|
563
|
+
|
|
564
|
+
if first_space + 4 > status_line.len:
|
|
565
|
+
return Result[ResponseHead, Error].failure(error = response_error("missing status code"))
|
|
566
|
+
|
|
567
|
+
let status_code_text = status_line.slice(first_space + 1, 3)
|
|
568
|
+
var status_code = 0
|
|
569
|
+
match parse_decimal(status_code_text):
|
|
570
|
+
Option.none:
|
|
571
|
+
return Result[ResponseHead, Error].failure(error = response_error("invalid status code"))
|
|
572
|
+
Option.some as payload:
|
|
573
|
+
status_code = int<-payload.value
|
|
574
|
+
|
|
575
|
+
var reason = string.String.create()
|
|
576
|
+
if first_space + 4 < status_line.len:
|
|
577
|
+
if status_line.byte_at(first_space + 4) != 32:
|
|
578
|
+
return Result[
|
|
579
|
+
ResponseHead,
|
|
580
|
+
Error
|
|
581
|
+
].failure(error = response_error("status line must separate code and reason with a space"))
|
|
582
|
+
|
|
583
|
+
let reason_start = first_space + 5
|
|
584
|
+
reason = string.String.from_str(status_line.slice(reason_start, status_line.len - reason_start))
|
|
585
|
+
|
|
586
|
+
var head = ResponseHead(
|
|
587
|
+
status_code = status_code,
|
|
588
|
+
reason = reason,
|
|
589
|
+
headers = vec.Vec[Header].create(),
|
|
590
|
+
content_length = Option[ptr_uint].none,
|
|
591
|
+
chunked = false
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
var index = header_index
|
|
595
|
+
while index < header_text.len:
|
|
596
|
+
var next_line_end = header_text.len
|
|
597
|
+
match find_crlf(header_text, index):
|
|
598
|
+
Option.none:
|
|
599
|
+
pass
|
|
600
|
+
Option.some as payload:
|
|
601
|
+
next_line_end = payload.value
|
|
602
|
+
|
|
603
|
+
let line = header_text.slice(index, next_line_end - index)
|
|
604
|
+
if line.len == 0:
|
|
605
|
+
head.release()
|
|
606
|
+
return Result[ResponseHead, Error].failure(error = response_error("unexpected blank header line"))
|
|
607
|
+
|
|
608
|
+
var separator: ptr_uint = 0
|
|
609
|
+
match line.find_byte(58):
|
|
610
|
+
Option.none:
|
|
611
|
+
head.release()
|
|
612
|
+
return Result[ResponseHead, Error].failure(error = response_error("header line is missing ':'"))
|
|
613
|
+
Option.some as payload:
|
|
614
|
+
separator = payload.value
|
|
615
|
+
|
|
616
|
+
if separator == 0:
|
|
617
|
+
head.release()
|
|
618
|
+
return Result[ResponseHead, Error].failure(error = response_error("header name is empty"))
|
|
619
|
+
|
|
620
|
+
let name_text = line.slice(0, separator)
|
|
621
|
+
let value_start = separator + 1
|
|
622
|
+
let value_text = line.slice(value_start, line.len - value_start).trim_ascii_whitespace()
|
|
623
|
+
|
|
624
|
+
head.headers.push(Header(name = string.String.from_str(name_text), value = string.String.from_str(value_text)))
|
|
625
|
+
|
|
626
|
+
if ascii_case_equal(name_text, "Content-Length"):
|
|
627
|
+
match parse_decimal(value_text):
|
|
628
|
+
Option.none:
|
|
629
|
+
head.release()
|
|
630
|
+
return Result[
|
|
631
|
+
ResponseHead,
|
|
632
|
+
Error
|
|
633
|
+
].failure(error = response_error("Content-Length must be a decimal integer"))
|
|
634
|
+
Option.some as payload:
|
|
635
|
+
head.content_length = Option[ptr_uint].some(value = payload.value)
|
|
636
|
+
|
|
637
|
+
if ascii_case_equal(name_text, "Transfer-Encoding"):
|
|
638
|
+
if ascii_case_equal(value_text, "chunked"):
|
|
639
|
+
head.chunked = true
|
|
640
|
+
else:
|
|
641
|
+
head.release()
|
|
642
|
+
return Result[ResponseHead, Error].failure(error = response_error("unsupported Transfer-Encoding"))
|
|
643
|
+
|
|
644
|
+
if next_line_end == header_text.len:
|
|
645
|
+
index = header_text.len
|
|
646
|
+
else:
|
|
647
|
+
index = next_line_end + 2
|
|
648
|
+
|
|
649
|
+
if head.chunked:
|
|
650
|
+
head.content_length = Option[ptr_uint].none
|
|
651
|
+
|
|
652
|
+
return Result[ResponseHead, Error].success(value = head)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
function decode_buffered_chunked_body(encoded: span[ubyte]) -> Result[bytes.Bytes, Error]:
|
|
656
|
+
var body = vec.Vec[ubyte].with_capacity(encoded.len)
|
|
657
|
+
defer body.release()
|
|
658
|
+
|
|
659
|
+
var cursor: ptr_uint = 0
|
|
660
|
+
while true:
|
|
661
|
+
let line_end = find_crlf_bytes(encoded, cursor) else:
|
|
662
|
+
return Result[
|
|
663
|
+
bytes.Bytes,
|
|
664
|
+
Error
|
|
665
|
+
].failure(error = response_error("chunked response ended before chunk size"))
|
|
666
|
+
|
|
667
|
+
let line_bytes = unsafe: span[ubyte](data = encoded.data + cursor, len = line_end - cursor)
|
|
668
|
+
let line_text = text.utf8_byte_span_as_str(line_bytes) else:
|
|
669
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("chunk size line is not valid UTF-8"))
|
|
670
|
+
|
|
671
|
+
let chunk_size = parse_chunk_size(line_text) else:
|
|
672
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("invalid chunk size"))
|
|
673
|
+
|
|
674
|
+
cursor = line_end + 2
|
|
675
|
+
|
|
676
|
+
if chunk_size == 0:
|
|
677
|
+
while true:
|
|
678
|
+
let trailer_end = find_crlf_bytes(encoded, cursor) else:
|
|
679
|
+
return Result[
|
|
680
|
+
bytes.Bytes,
|
|
681
|
+
Error
|
|
682
|
+
].failure(error = response_error("chunked response ended before trailers were complete"))
|
|
683
|
+
|
|
684
|
+
if trailer_end == cursor:
|
|
685
|
+
if trailer_end + 2 != encoded.len:
|
|
686
|
+
return Result[
|
|
687
|
+
bytes.Bytes,
|
|
688
|
+
Error
|
|
689
|
+
].failure(error = response_error("unexpected bytes after chunked body"))
|
|
690
|
+
|
|
691
|
+
return Result[bytes.Bytes, Error].success(value = bytes.Bytes.copy(body.as_span()))
|
|
692
|
+
|
|
693
|
+
cursor = trailer_end + 2
|
|
694
|
+
|
|
695
|
+
if encoded.len < cursor or encoded.len - cursor < chunk_size:
|
|
696
|
+
return Result[
|
|
697
|
+
bytes.Bytes,
|
|
698
|
+
Error
|
|
699
|
+
].failure(error = response_error("chunked response ended before chunk data"))
|
|
700
|
+
|
|
701
|
+
let remaining = encoded.len - cursor - chunk_size
|
|
702
|
+
if remaining < 2:
|
|
703
|
+
return Result[
|
|
704
|
+
bytes.Bytes,
|
|
705
|
+
Error
|
|
706
|
+
].failure(error = response_error("chunked response ended before chunk terminator"))
|
|
707
|
+
|
|
708
|
+
let chunk_bytes = unsafe: span[ubyte](data = encoded.data + cursor, len = chunk_size)
|
|
709
|
+
body.append_span(chunk_bytes)
|
|
710
|
+
cursor += chunk_size
|
|
711
|
+
|
|
712
|
+
if unsafe: read(encoded.data + cursor) != 13 or read(encoded.data + cursor + 1) != 10:
|
|
713
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("chunk data must end with CRLF"))
|
|
714
|
+
|
|
715
|
+
cursor += 2
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
function parse_buffered_body(
|
|
719
|
+
body_bytes: span[ubyte],
|
|
720
|
+
content_length: Option[ptr_uint],
|
|
721
|
+
chunked: bool
|
|
722
|
+
) -> Result[bytes.Bytes, Error]:
|
|
723
|
+
if chunked:
|
|
724
|
+
return decode_buffered_chunked_body(body_bytes)
|
|
725
|
+
|
|
726
|
+
match content_length:
|
|
727
|
+
Option.none:
|
|
728
|
+
return Result[bytes.Bytes, Error].success(value = bytes.Bytes.copy(body_bytes))
|
|
729
|
+
Option.some as payload:
|
|
730
|
+
if body_bytes.len != payload.value:
|
|
731
|
+
return Result[
|
|
732
|
+
bytes.Bytes,
|
|
733
|
+
Error
|
|
734
|
+
].failure(error = response_error("body length did not match Content-Length"))
|
|
735
|
+
|
|
736
|
+
return Result[bytes.Bytes, Error].success(value = bytes.Bytes.copy(body_bytes))
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
function parse_buffered_response(raw_response: span[ubyte]) -> Result[Response, Error]:
|
|
740
|
+
let header_length = find_header_terminator(raw_response) else:
|
|
741
|
+
return Result[Response, Error].failure(error = response_error("response ended before headers were complete"))
|
|
742
|
+
|
|
743
|
+
let header_bytes = span[ubyte](data = raw_response.data, len = header_length)
|
|
744
|
+
let header_text = text.utf8_byte_span_as_str(header_bytes) else:
|
|
745
|
+
return Result[Response, Error].failure(error = response_error("headers are not valid UTF-8"))
|
|
746
|
+
|
|
747
|
+
let head_result = parse_response_head(header_text)
|
|
748
|
+
match head_result:
|
|
749
|
+
Result.failure as payload:
|
|
750
|
+
return Result[Response, Error].failure(error = payload.error)
|
|
751
|
+
Result.success as payload:
|
|
752
|
+
var head = payload.value
|
|
753
|
+
let body_start = header_length + 4
|
|
754
|
+
let body_bytes = unsafe: span[ubyte](
|
|
755
|
+
data = raw_response.data + body_start,
|
|
756
|
+
len = raw_response.len - body_start
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
let body_result = parse_buffered_body(body_bytes, head.content_length, head.chunked)
|
|
760
|
+
match body_result:
|
|
761
|
+
Result.failure as body_payload:
|
|
762
|
+
head.release()
|
|
763
|
+
return Result[Response, Error].failure(error = body_payload.error)
|
|
764
|
+
Result.success as body_payload:
|
|
765
|
+
let body = body_payload.value
|
|
766
|
+
return Result[Response, Error].success(
|
|
767
|
+
value = Response(
|
|
768
|
+
status_code = head.status_code,
|
|
769
|
+
reason = head.reason,
|
|
770
|
+
headers = head.headers,
|
|
771
|
+
body = body
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
async function read_chunked_body(stream: net.TcpStream, prefix: span[ubyte]) -> Result[bytes.Bytes, Error]:
|
|
777
|
+
var body = vec.Vec[ubyte].with_capacity(prefix.len)
|
|
778
|
+
defer body.release()
|
|
779
|
+
|
|
780
|
+
var buffer = vec.Vec[ubyte].with_capacity(prefix.len + 64)
|
|
781
|
+
defer buffer.release()
|
|
782
|
+
buffer.append_span(prefix)
|
|
783
|
+
|
|
784
|
+
var cursor: ptr_uint = 0
|
|
785
|
+
while true:
|
|
786
|
+
var line_end: ptr_uint = 0
|
|
787
|
+
while true:
|
|
788
|
+
match find_crlf_bytes(buffer.as_span(), cursor):
|
|
789
|
+
Option.none:
|
|
790
|
+
if cursor > 0:
|
|
791
|
+
discard_buffer_prefix(ref_of(buffer), cursor)
|
|
792
|
+
cursor = 0
|
|
793
|
+
|
|
794
|
+
let chunk_result = await stream.read_once(4096)
|
|
795
|
+
match chunk_result:
|
|
796
|
+
Result.failure as error_payload:
|
|
797
|
+
return Result[bytes.Bytes, Error].failure(error = status_net_error(error_payload.error))
|
|
798
|
+
Result.success as ok_payload:
|
|
799
|
+
var chunk = ok_payload.value
|
|
800
|
+
if chunk.len == 0:
|
|
801
|
+
chunk.release()
|
|
802
|
+
return Result[
|
|
803
|
+
bytes.Bytes,
|
|
804
|
+
Error
|
|
805
|
+
].failure(error = response_error("chunked response ended before chunk size"))
|
|
806
|
+
|
|
807
|
+
buffer.append_span(chunk.as_span())
|
|
808
|
+
chunk.release()
|
|
809
|
+
continue
|
|
810
|
+
Option.some as payload:
|
|
811
|
+
line_end = payload.value
|
|
812
|
+
break
|
|
813
|
+
|
|
814
|
+
let line_bytes = unsafe: span[ubyte](data = buffer.as_span().data + cursor, len = line_end - cursor)
|
|
815
|
+
let line_text = text.utf8_byte_span_as_str(line_bytes) else:
|
|
816
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("chunk size line is not valid UTF-8"))
|
|
817
|
+
|
|
818
|
+
let chunk_size = parse_chunk_size(line_text) else:
|
|
819
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("invalid chunk size"))
|
|
820
|
+
|
|
821
|
+
cursor = line_end + 2
|
|
822
|
+
|
|
823
|
+
if chunk_size == 0:
|
|
824
|
+
while true:
|
|
825
|
+
var trailer_end: ptr_uint = 0
|
|
826
|
+
while true:
|
|
827
|
+
match find_crlf_bytes(buffer.as_span(), cursor):
|
|
828
|
+
Option.none:
|
|
829
|
+
if cursor > 0:
|
|
830
|
+
discard_buffer_prefix(ref_of(buffer), cursor)
|
|
831
|
+
cursor = 0
|
|
832
|
+
|
|
833
|
+
let chunk_result = await stream.read_once(4096)
|
|
834
|
+
match chunk_result:
|
|
835
|
+
Result.failure as error_payload:
|
|
836
|
+
return Result[
|
|
837
|
+
bytes.Bytes,
|
|
838
|
+
Error
|
|
839
|
+
].failure(error = status_net_error(error_payload.error))
|
|
840
|
+
Result.success as ok_payload:
|
|
841
|
+
var chunk = ok_payload.value
|
|
842
|
+
if chunk.len == 0:
|
|
843
|
+
chunk.release()
|
|
844
|
+
return Result[
|
|
845
|
+
bytes.Bytes,
|
|
846
|
+
Error
|
|
847
|
+
].failure(error = response_error("chunked response ended before trailers were complete"))
|
|
848
|
+
|
|
849
|
+
buffer.append_span(chunk.as_span())
|
|
850
|
+
chunk.release()
|
|
851
|
+
continue
|
|
852
|
+
Option.some as payload:
|
|
853
|
+
trailer_end = payload.value
|
|
854
|
+
break
|
|
855
|
+
|
|
856
|
+
if trailer_end == cursor:
|
|
857
|
+
return Result[bytes.Bytes, Error].success(value = bytes.Bytes.copy(body.as_span()))
|
|
858
|
+
|
|
859
|
+
cursor = trailer_end + 2
|
|
860
|
+
|
|
861
|
+
while buffer.len - cursor < chunk_size + 2:
|
|
862
|
+
if cursor > 0:
|
|
863
|
+
discard_buffer_prefix(ref_of(buffer), cursor)
|
|
864
|
+
cursor = 0
|
|
865
|
+
|
|
866
|
+
let chunk_result = await stream.read_once(4096)
|
|
867
|
+
match chunk_result:
|
|
868
|
+
Result.failure as error_payload:
|
|
869
|
+
return Result[bytes.Bytes, Error].failure(error = status_net_error(error_payload.error))
|
|
870
|
+
Result.success as ok_payload:
|
|
871
|
+
var chunk = ok_payload.value
|
|
872
|
+
if chunk.len == 0:
|
|
873
|
+
chunk.release()
|
|
874
|
+
return Result[
|
|
875
|
+
bytes.Bytes,
|
|
876
|
+
Error
|
|
877
|
+
].failure(error = response_error("chunked response ended before chunk data"))
|
|
878
|
+
|
|
879
|
+
buffer.append_span(chunk.as_span())
|
|
880
|
+
chunk.release()
|
|
881
|
+
|
|
882
|
+
let chunk_bytes = unsafe: span[ubyte](data = buffer.as_span().data + cursor, len = chunk_size)
|
|
883
|
+
body.append_span(chunk_bytes)
|
|
884
|
+
cursor += chunk_size
|
|
885
|
+
|
|
886
|
+
let unread = buffer.as_span()
|
|
887
|
+
if unsafe: read(unread.data + cursor) != 13 or read(unread.data + cursor + 1) != 10:
|
|
888
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("chunk data must end with CRLF"))
|
|
889
|
+
|
|
890
|
+
cursor += 2
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
async function read_body(
|
|
894
|
+
stream: net.TcpStream,
|
|
895
|
+
prefix: span[ubyte],
|
|
896
|
+
content_length: Option[ptr_uint],
|
|
897
|
+
chunked: bool
|
|
898
|
+
) -> Result[bytes.Bytes, Error]:
|
|
899
|
+
if chunked:
|
|
900
|
+
return await read_chunked_body(stream, prefix)
|
|
901
|
+
|
|
902
|
+
match content_length:
|
|
903
|
+
Option.some as payload:
|
|
904
|
+
let total_length = payload.value
|
|
905
|
+
if prefix.len > total_length:
|
|
906
|
+
return Result[bytes.Bytes, Error].failure(error = response_error("body exceeded Content-Length"))
|
|
907
|
+
|
|
908
|
+
var body = vec.Vec[ubyte].with_capacity(total_length)
|
|
909
|
+
defer body.release()
|
|
910
|
+
|
|
911
|
+
body.append_span(prefix)
|
|
912
|
+
|
|
913
|
+
let remaining = total_length - prefix.len
|
|
914
|
+
if remaining > 0:
|
|
915
|
+
let chunk_result = await stream.read_exactly(remaining)
|
|
916
|
+
match chunk_result:
|
|
917
|
+
Result.failure as error_payload:
|
|
918
|
+
return Result[bytes.Bytes, Error].failure(error = status_net_error(error_payload.error))
|
|
919
|
+
Result.success as ok_payload:
|
|
920
|
+
var chunk = ok_payload.value
|
|
921
|
+
body.append_span(chunk.as_span())
|
|
922
|
+
chunk.release()
|
|
923
|
+
|
|
924
|
+
return Result[bytes.Bytes, Error].success(value = bytes.Bytes.copy(body.as_span()))
|
|
925
|
+
Option.none:
|
|
926
|
+
var body = vec.Vec[ubyte].with_capacity(prefix.len)
|
|
927
|
+
defer body.release()
|
|
928
|
+
|
|
929
|
+
body.append_span(prefix)
|
|
930
|
+
|
|
931
|
+
while true:
|
|
932
|
+
let chunk_result = await stream.read_once(4096)
|
|
933
|
+
match chunk_result:
|
|
934
|
+
Result.failure as error_payload:
|
|
935
|
+
return Result[bytes.Bytes, Error].failure(error = status_net_error(error_payload.error))
|
|
936
|
+
Result.success as ok_payload:
|
|
937
|
+
var chunk = ok_payload.value
|
|
938
|
+
if chunk.len == 0:
|
|
939
|
+
chunk.release()
|
|
940
|
+
break
|
|
941
|
+
|
|
942
|
+
body.append_span(chunk.as_span())
|
|
943
|
+
chunk.release()
|
|
944
|
+
|
|
945
|
+
return Result[bytes.Bytes, Error].success(value = bytes.Bytes.copy(body.as_span()))
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
async function read_response(stream: net.TcpStream) -> Result[Response, Error]:
|
|
949
|
+
var received = vec.Vec[ubyte].create()
|
|
950
|
+
defer received.release()
|
|
951
|
+
|
|
952
|
+
var header_length: ptr_uint = 0
|
|
953
|
+
var headers_ready = false
|
|
954
|
+
while not headers_ready:
|
|
955
|
+
let chunk_result = await stream.read_once(4096)
|
|
956
|
+
match chunk_result:
|
|
957
|
+
Result.failure as error_payload:
|
|
958
|
+
return Result[Response, Error].failure(error = status_net_error(error_payload.error))
|
|
959
|
+
Result.success as ok_payload:
|
|
960
|
+
var chunk = ok_payload.value
|
|
961
|
+
if chunk.len == 0:
|
|
962
|
+
chunk.release()
|
|
963
|
+
return Result[
|
|
964
|
+
Response,
|
|
965
|
+
Error
|
|
966
|
+
].failure(error = response_error("response ended before headers were complete"))
|
|
967
|
+
|
|
968
|
+
received.append_span(chunk.as_span())
|
|
969
|
+
chunk.release()
|
|
970
|
+
|
|
971
|
+
match find_header_terminator(received.as_span()):
|
|
972
|
+
Option.none:
|
|
973
|
+
continue
|
|
974
|
+
Option.some as payload:
|
|
975
|
+
header_length = payload.value
|
|
976
|
+
headers_ready = true
|
|
977
|
+
|
|
978
|
+
let raw = received.as_span()
|
|
979
|
+
let header_bytes = span[ubyte](data = raw.data, len = header_length)
|
|
980
|
+
let header_text = text.utf8_byte_span_as_str(header_bytes) else:
|
|
981
|
+
return Result[Response, Error].failure(error = response_error("headers are not valid UTF-8"))
|
|
982
|
+
|
|
983
|
+
let head_result = parse_response_head(header_text)
|
|
984
|
+
match head_result:
|
|
985
|
+
Result.failure as payload:
|
|
986
|
+
return Result[Response, Error].failure(error = payload.error)
|
|
987
|
+
Result.success as payload:
|
|
988
|
+
var head = payload.value
|
|
989
|
+
let body_start = header_length + 4
|
|
990
|
+
let body_prefix = unsafe: span[ubyte](data = raw.data + body_start, len = raw.len - body_start)
|
|
991
|
+
|
|
992
|
+
let body_result = await read_body(stream, body_prefix, head.content_length, head.chunked)
|
|
993
|
+
match body_result:
|
|
994
|
+
Result.failure as body_error_payload:
|
|
995
|
+
head.release()
|
|
996
|
+
return Result[Response, Error].failure(error = body_error_payload.error)
|
|
997
|
+
Result.success as body_payload:
|
|
998
|
+
let body = body_payload.value
|
|
999
|
+
return Result[Response, Error].success(
|
|
1000
|
+
value = Response(
|
|
1001
|
+
status_code = head.status_code,
|
|
1002
|
+
reason = head.reason,
|
|
1003
|
+
headers = head.headers,
|
|
1004
|
+
body = body
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
async function request_http(parsed: ParsedUrl, request_bytes: span[ubyte]) -> Result[Response, Error]:
|
|
1010
|
+
var service = fmt.to_string_int(parsed.port)
|
|
1011
|
+
defer service.release()
|
|
1012
|
+
|
|
1013
|
+
let address_result = await net.resolve_first(parsed.host.as_str(), service.as_str())
|
|
1014
|
+
match address_result:
|
|
1015
|
+
Result.failure as address_error_payload:
|
|
1016
|
+
return Result[Response, Error].failure(error = status_net_error(address_error_payload.error))
|
|
1017
|
+
Result.success as address_payload:
|
|
1018
|
+
var address = address_payload.value
|
|
1019
|
+
defer address.release()
|
|
1020
|
+
|
|
1021
|
+
let connect_result = await net.connect(address)
|
|
1022
|
+
match connect_result:
|
|
1023
|
+
Result.failure as connect_error_payload:
|
|
1024
|
+
return Result[Response, Error].failure(error = status_net_error(connect_error_payload.error))
|
|
1025
|
+
Result.success as connect_payload:
|
|
1026
|
+
var stream = connect_payload.value
|
|
1027
|
+
defer stream.release()
|
|
1028
|
+
|
|
1029
|
+
let write_result = await stream.write_bytes(request_bytes)
|
|
1030
|
+
match write_result:
|
|
1031
|
+
Result.failure as write_error_payload:
|
|
1032
|
+
return Result[Response, Error].failure(error = status_net_error(write_error_payload.error))
|
|
1033
|
+
Result.success as write_payload:
|
|
1034
|
+
if write_payload.value != request_bytes.len:
|
|
1035
|
+
return Result[
|
|
1036
|
+
Response,
|
|
1037
|
+
Error
|
|
1038
|
+
].failure(error = status_error("http request write did not send the full request"))
|
|
1039
|
+
|
|
1040
|
+
return await read_response(stream)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
async function read_buffered_tls_response(stream: tls.Stream) -> Result[Response, Error]:
|
|
1044
|
+
var received = vec.Vec[ubyte].create()
|
|
1045
|
+
defer received.release()
|
|
1046
|
+
|
|
1047
|
+
while true:
|
|
1048
|
+
let chunk_result = await stream.read_once(4096)
|
|
1049
|
+
match chunk_result:
|
|
1050
|
+
Result.failure as error_payload:
|
|
1051
|
+
return Result[Response, Error].failure(error = status_tls_error(error_payload.error))
|
|
1052
|
+
Result.success as chunk_payload:
|
|
1053
|
+
var chunk = chunk_payload.value
|
|
1054
|
+
if chunk.len == 0:
|
|
1055
|
+
chunk.release()
|
|
1056
|
+
break
|
|
1057
|
+
|
|
1058
|
+
received.append_span(chunk.as_span())
|
|
1059
|
+
chunk.release()
|
|
1060
|
+
|
|
1061
|
+
return parse_buffered_response(received.as_span())
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
async function request_https(parsed: ParsedUrl, request_bytes: span[ubyte]) -> Result[Response, Error]:
|
|
1065
|
+
let connect_result = await tls.connect(parsed.host.as_str(), parsed.port)
|
|
1066
|
+
match connect_result:
|
|
1067
|
+
Result.failure as payload:
|
|
1068
|
+
return Result[Response, Error].failure(error = status_tls_error(payload.error))
|
|
1069
|
+
Result.success as payload:
|
|
1070
|
+
var stream = payload.value
|
|
1071
|
+
defer stream.release()
|
|
1072
|
+
|
|
1073
|
+
let write_result = await stream.write_bytes(request_bytes)
|
|
1074
|
+
match write_result:
|
|
1075
|
+
Result.failure as write_error_payload:
|
|
1076
|
+
return Result[Response, Error].failure(error = status_tls_error(write_error_payload.error))
|
|
1077
|
+
Result.success as write_payload:
|
|
1078
|
+
if write_payload.value != request_bytes.len:
|
|
1079
|
+
return Result[
|
|
1080
|
+
Response,
|
|
1081
|
+
Error
|
|
1082
|
+
].failure(error = status_error("https request write did not send the full request"))
|
|
1083
|
+
|
|
1084
|
+
return await read_buffered_tls_response(stream)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
async function request_with_transport(parsed: ParsedUrl, request_bytes: span[ubyte]) -> Result[Response, Error]:
|
|
1088
|
+
match parsed.scheme:
|
|
1089
|
+
UrlScheme.http:
|
|
1090
|
+
return await request_http(parsed, request_bytes)
|
|
1091
|
+
UrlScheme.https:
|
|
1092
|
+
return await request_https(parsed, request_bytes)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
extending Error:
|
|
1096
|
+
public editable function release() -> void:
|
|
1097
|
+
this.message.release()
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
extending Header:
|
|
1101
|
+
public editable function release() -> void:
|
|
1102
|
+
this.name.release()
|
|
1103
|
+
this.value.release()
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
extending Response:
|
|
1107
|
+
public editable function release() -> void:
|
|
1108
|
+
this.reason.release()
|
|
1109
|
+
|
|
1110
|
+
var index: ptr_uint = 0
|
|
1111
|
+
while index < this.headers.len:
|
|
1112
|
+
let current = this.headers.get(index) else:
|
|
1113
|
+
fatal(c"http.Response.release missing header")
|
|
1114
|
+
|
|
1115
|
+
var header = unsafe: read(current)
|
|
1116
|
+
header.release()
|
|
1117
|
+
index += 1
|
|
1118
|
+
|
|
1119
|
+
this.headers.release()
|
|
1120
|
+
this.body.release()
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
public function body_as_str() -> Option[str]:
|
|
1124
|
+
return this.body.as_str()
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
public function header(name: str) -> Option[str]:
|
|
1128
|
+
var index: ptr_uint = 0
|
|
1129
|
+
while index < this.headers.len:
|
|
1130
|
+
let current = this.headers.get(index) else:
|
|
1131
|
+
fatal(c"http.Response.header missing header")
|
|
1132
|
+
|
|
1133
|
+
let header_value = unsafe: read(current)
|
|
1134
|
+
if ascii_case_equal(header_value.name.as_str(), name):
|
|
1135
|
+
return Option[str].some(value = header_value.value.as_str())
|
|
1136
|
+
|
|
1137
|
+
index += 1
|
|
1138
|
+
|
|
1139
|
+
return Option[str].none
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
extending ParsedUrl:
|
|
1143
|
+
editable function release() -> void:
|
|
1144
|
+
this.host.release()
|
|
1145
|
+
this.authority.release()
|
|
1146
|
+
this.target.release()
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
extending ResponseHead:
|
|
1150
|
+
editable function release() -> void:
|
|
1151
|
+
this.reason.release()
|
|
1152
|
+
|
|
1153
|
+
var index: ptr_uint = 0
|
|
1154
|
+
while index < this.headers.len:
|
|
1155
|
+
let current = this.headers.get(index) else:
|
|
1156
|
+
fatal(c"http.ResponseHead.release missing header")
|
|
1157
|
+
|
|
1158
|
+
var header = unsafe: read(current)
|
|
1159
|
+
header.release()
|
|
1160
|
+
index += 1
|
|
1161
|
+
|
|
1162
|
+
this.headers.release()
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
public async function get(url: str) -> Result[Response, Error]:
|
|
1166
|
+
return await request(url, "GET", zero[span[RequestHeader]], Option[span[ubyte]].none)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
public async function head(url: str) -> Result[Response, Error]:
|
|
1170
|
+
return await request(url, "HEAD", zero[span[RequestHeader]], Option[span[ubyte]].none)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
public async function delete(url: str) -> Result[Response, Error]:
|
|
1174
|
+
return await request(url, "DELETE", zero[span[RequestHeader]], Option[span[ubyte]].none)
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
public async function options(url: str) -> Result[Response, Error]:
|
|
1178
|
+
return await request(url, "OPTIONS", zero[span[RequestHeader]], Option[span[ubyte]].none)
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
async function request_with_body(
|
|
1182
|
+
url: str,
|
|
1183
|
+
method: str,
|
|
1184
|
+
body: str,
|
|
1185
|
+
content_type: str
|
|
1186
|
+
) -> Result[Response, Error]:
|
|
1187
|
+
var ct = RequestHeader(name = "Content-Type", value = content_type)
|
|
1188
|
+
let headers = span[RequestHeader](data = ptr_of(ct), len = 1)
|
|
1189
|
+
let body_bytes = text.as_byte_span(body)
|
|
1190
|
+
return await request(url, method, headers, Option[span[ubyte]].some(value = body_bytes))
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
public async function post(url: str, body: str, content_type: str) -> Result[Response, Error]:
|
|
1194
|
+
return await request_with_body(url, "POST", body, content_type)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
public async function put(url: str, body: str, content_type: str) -> Result[Response, Error]:
|
|
1198
|
+
return await request_with_body(url, "PUT", body, content_type)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
public async function patch(url: str, body: str, content_type: str) -> Result[Response, Error]:
|
|
1202
|
+
return await request_with_body(url, "PATCH", body, content_type)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
public async function request(
|
|
1206
|
+
url: str,
|
|
1207
|
+
method: str,
|
|
1208
|
+
headers: span[RequestHeader],
|
|
1209
|
+
body: Option[span[ubyte]]
|
|
1210
|
+
) -> Result[Response, Error]:
|
|
1211
|
+
let parsed_result = parse_url(url)
|
|
1212
|
+
match parsed_result:
|
|
1213
|
+
Result.failure as payload:
|
|
1214
|
+
return Result[Response, Error].failure(error = payload.error)
|
|
1215
|
+
Result.success as payload:
|
|
1216
|
+
var parsed = payload.value
|
|
1217
|
+
defer parsed.release()
|
|
1218
|
+
|
|
1219
|
+
let request_result = build_request(parsed, method, headers, body)
|
|
1220
|
+
match request_result:
|
|
1221
|
+
Result.failure as request_error_payload:
|
|
1222
|
+
return Result[Response, Error].failure(error = request_error_payload.error)
|
|
1223
|
+
Result.success as request_payload:
|
|
1224
|
+
var request_bytes = request_payload.value
|
|
1225
|
+
defer request_bytes.release()
|
|
1226
|
+
|
|
1227
|
+
return await request_with_transport(parsed, request_bytes.as_span())
|