castrum 0.9.3 → 0.9.4
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.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
package/rust/ffi/http.rs
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// rust/ffi/http.rs — HTTP wire-format C-ABI exports.
|
|
2
|
+
//
|
|
3
|
+
// Cache semantics (etag / conditional / http-date), media-type + accept-encoding
|
|
4
|
+
// negotiation, MIME lookup, URL resolve/encode, and the packed parsers
|
|
5
|
+
// (http request / query / cookie / form / multipart).
|
|
6
|
+
|
|
7
|
+
use std::slice;
|
|
8
|
+
|
|
9
|
+
use super::util::{cstring_return, panic_guard};
|
|
10
|
+
|
|
11
|
+
/// crc32-based ETag (10 strong / 12 weak) into `out`; `weak` is a u8 flag.
|
|
12
|
+
///
|
|
13
|
+
/// # Safety
|
|
14
|
+
/// ETag (`"<8-hex>"` or `W/"<8-hex>"`) returned as a null-terminated C string
|
|
15
|
+
/// into the per-thread reused buffer (`cstring` return — the engine clones it).
|
|
16
|
+
///
|
|
17
|
+
/// # Safety
|
|
18
|
+
/// `data` must be valid for reads of `len` bytes.
|
|
19
|
+
#[no_mangle]
|
|
20
|
+
pub unsafe extern "C" fn castrum_etag(
|
|
21
|
+
data: *const u8,
|
|
22
|
+
len: usize,
|
|
23
|
+
weak: u8,
|
|
24
|
+
) -> *const std::os::raw::c_char {
|
|
25
|
+
if data.is_null() {
|
|
26
|
+
return std::ptr::null();
|
|
27
|
+
}
|
|
28
|
+
let crc = crc32fast::hash(slice::from_raw_parts(data, len));
|
|
29
|
+
let needed = if weak != 0 { 12 } else { 10 };
|
|
30
|
+
cstring_return(needed, |out| {
|
|
31
|
+
crate::http::etag::etag_from_crc32_into(crc, weak != 0, out).ok()
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// crc32-based ETag written directly into a caller buffer — the pooled sibling
|
|
36
|
+
/// of `castrum_etag` (no cstring round-trip). Writes 10 strong / 12 weak bytes;
|
|
37
|
+
/// returns bytes written, the exact required size when `out_cap` is too small,
|
|
38
|
+
/// or 0 on invalid input.
|
|
39
|
+
///
|
|
40
|
+
/// # Safety
|
|
41
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
42
|
+
#[no_mangle]
|
|
43
|
+
pub unsafe extern "C" fn castrum_etag_into(
|
|
44
|
+
data: *const u8,
|
|
45
|
+
len: usize,
|
|
46
|
+
weak: u8,
|
|
47
|
+
out: *mut u8,
|
|
48
|
+
out_cap: usize,
|
|
49
|
+
) -> usize {
|
|
50
|
+
if data.is_null() || out.is_null() {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
let needed = if weak != 0 { 12 } else { 10 };
|
|
54
|
+
if out_cap < needed {
|
|
55
|
+
return needed;
|
|
56
|
+
}
|
|
57
|
+
let crc = crc32fast::hash(slice::from_raw_parts(data, len));
|
|
58
|
+
crate::http::etag::etag_from_crc32_into(crc, weak != 0, slice::from_raw_parts_mut(out, needed))
|
|
59
|
+
.unwrap_or_default()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Evaluate `ConditionalRequest::is_not_modified` against the precompiled state
|
|
63
|
+
/// via its opaque inner handle (from the napi `inner_ptr()`). `flags` bit0 =
|
|
64
|
+
/// If-None-Match present, bit1 = If-Modified-Since present (a present-but-empty
|
|
65
|
+
/// header is distinct from absent, matching the napi `Option` semantics).
|
|
66
|
+
/// Returns 1 → 304. A null handle (0) → 0, so a dropped instance can never
|
|
67
|
+
/// dereference freed state.
|
|
68
|
+
///
|
|
69
|
+
/// `inm`/`ims` cross as `(ptr, len)` byte slices — the same convention as every
|
|
70
|
+
/// other instance op (`castrum_media_type_matcher_matches`, `castrum_jwt_signer_*`,
|
|
71
|
+
/// `castrum_template_render`, `castrum_schema_validator_validate`) — so the JS
|
|
72
|
+
/// side passes arbitrary header bytes with no NUL-termination requirement.
|
|
73
|
+
/// Presence is gated by `flags`, so the pointers are ignored when the
|
|
74
|
+
/// corresponding bit is 0.
|
|
75
|
+
///
|
|
76
|
+
/// # Safety
|
|
77
|
+
/// `inner` must be a valid `ConditionalRequest` pointer obtained from
|
|
78
|
+
/// `inner_ptr()` and must stay alive for the call (the JS wrapper holds the
|
|
79
|
+
/// napi instance). `inm`/`ims` must be valid for `inm_len`/`ims_len` bytes when
|
|
80
|
+
/// the corresponding `flags` bit is set.
|
|
81
|
+
#[no_mangle]
|
|
82
|
+
pub unsafe extern "C" fn castrum_conditional_is_not_modified(
|
|
83
|
+
inner: usize,
|
|
84
|
+
inm: *const u8,
|
|
85
|
+
inm_len: usize,
|
|
86
|
+
ims: *const u8,
|
|
87
|
+
ims_len: usize,
|
|
88
|
+
flags: u8,
|
|
89
|
+
) -> u8 {
|
|
90
|
+
if inner == 0 {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
let inm_opt = if flags & 1 != 0 && !inm.is_null() {
|
|
94
|
+
Some(std::slice::from_raw_parts(inm, inm_len))
|
|
95
|
+
} else {
|
|
96
|
+
None
|
|
97
|
+
};
|
|
98
|
+
let ims_opt = if flags & 2 != 0 && !ims.is_null() {
|
|
99
|
+
Some(std::slice::from_raw_parts(ims, ims_len))
|
|
100
|
+
} else {
|
|
101
|
+
None
|
|
102
|
+
};
|
|
103
|
+
panic_guard(
|
|
104
|
+
|| {
|
|
105
|
+
u8::from(unsafe {
|
|
106
|
+
crate::http::etag::conditional_is_not_modified(
|
|
107
|
+
inner as *const crate::http::etag::ConditionalRequest,
|
|
108
|
+
inm_opt,
|
|
109
|
+
ims_opt,
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
0,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// MediaTypeMatcher: wildcard match against the PRECOMPILED expected type via
|
|
118
|
+
/// its opaque inner handle. Returns 1 = match. A null handle (0) → 0.
|
|
119
|
+
///
|
|
120
|
+
/// # Safety
|
|
121
|
+
/// `inner` must be a valid `MediaTypeMatcher` pointer from `inner_ptr()`, alive
|
|
122
|
+
/// for the call (the JS wrapper holds the napi instance).
|
|
123
|
+
#[no_mangle]
|
|
124
|
+
pub unsafe extern "C" fn castrum_media_type_matcher_matches(
|
|
125
|
+
inner: usize,
|
|
126
|
+
actual: *const u8,
|
|
127
|
+
actual_len: usize,
|
|
128
|
+
) -> u8 {
|
|
129
|
+
if inner == 0 || actual.is_null() {
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
let a = slice::from_raw_parts(actual, actual_len);
|
|
133
|
+
panic_guard(
|
|
134
|
+
|| {
|
|
135
|
+
u8::from(unsafe {
|
|
136
|
+
crate::http::media_type::media_type_matcher_matches_core(
|
|
137
|
+
inner as *const crate::http::media_type::MediaTypeMatcher,
|
|
138
|
+
a,
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
0,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// AcceptNegotiator: best supported encoding for `header` against the
|
|
147
|
+
/// PRECOMPILED supported list via its opaque inner handle → cstring (`null` =
|
|
148
|
+
/// identity, matching napi `Option<String>`).
|
|
149
|
+
///
|
|
150
|
+
/// `header` crosses as a `(ptr, len)` byte slice — the same convention as
|
|
151
|
+
/// `castrum_media_type_matcher_matches` — so the JS side passes arbitrary
|
|
152
|
+
/// header bytes with no NUL-termination requirement.
|
|
153
|
+
///
|
|
154
|
+
/// # Safety
|
|
155
|
+
/// `inner` must be a valid `AcceptNegotiator` pointer from `inner_ptr()`, alive
|
|
156
|
+
/// for the call; `header` must be valid for `header_len` bytes.
|
|
157
|
+
#[no_mangle]
|
|
158
|
+
pub unsafe extern "C" fn castrum_accept_negotiator_negotiate(
|
|
159
|
+
inner: usize,
|
|
160
|
+
header: *const u8,
|
|
161
|
+
header_len: usize,
|
|
162
|
+
) -> *const std::os::raw::c_char {
|
|
163
|
+
if inner == 0 || header.is_null() {
|
|
164
|
+
return std::ptr::null();
|
|
165
|
+
}
|
|
166
|
+
let h = slice::from_raw_parts(header, header_len);
|
|
167
|
+
let Some(bytes) = panic_guard(
|
|
168
|
+
|| unsafe {
|
|
169
|
+
crate::http::accept::accept_negotiator_negotiate_core(
|
|
170
|
+
inner as *const crate::http::accept::AcceptNegotiator,
|
|
171
|
+
h,
|
|
172
|
+
)
|
|
173
|
+
},
|
|
174
|
+
None,
|
|
175
|
+
) else {
|
|
176
|
+
return std::ptr::null();
|
|
177
|
+
};
|
|
178
|
+
cstring_return(bytes.len(), |out| {
|
|
179
|
+
out[..bytes.len()].copy_from_slice(&bytes);
|
|
180
|
+
Some(bytes.len())
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// AcceptNegotiator: best supported encoding for `header` with SERVER-
|
|
185
|
+
/// preference tie-breaking (ignex `negotiateEncoding` semantics — q-only, the
|
|
186
|
+
/// supported list's order breaks ties, empty header → identity). Same opaque
|
|
187
|
+
/// handle + cstring contract as `castrum_accept_negotiator_negotiate`.
|
|
188
|
+
///
|
|
189
|
+
/// # Safety
|
|
190
|
+
/// `inner` must be a valid `AcceptNegotiator` pointer from `inner_ptr()`, alive
|
|
191
|
+
/// for the call. `header` is a `bun:ffi` `cstring` ARG — a NUL-terminated C
|
|
192
|
+
/// string (the engine transcodes the JS header string in-engine; zero JS
|
|
193
|
+
/// encode), never NUL-containing.
|
|
194
|
+
#[no_mangle]
|
|
195
|
+
pub unsafe extern "C" fn castrum_accept_negotiator_negotiate_server(
|
|
196
|
+
inner: usize,
|
|
197
|
+
header: *const std::os::raw::c_char,
|
|
198
|
+
) -> *const std::os::raw::c_char {
|
|
199
|
+
if inner == 0 || header.is_null() {
|
|
200
|
+
return std::ptr::null();
|
|
201
|
+
}
|
|
202
|
+
let h = std::ffi::CStr::from_ptr(header).to_bytes();
|
|
203
|
+
let Some(bytes) = panic_guard(
|
|
204
|
+
|| unsafe {
|
|
205
|
+
crate::http::accept::accept_negotiator_negotiate_server_core(
|
|
206
|
+
inner as *const crate::http::accept::AcceptNegotiator,
|
|
207
|
+
h,
|
|
208
|
+
)
|
|
209
|
+
},
|
|
210
|
+
None,
|
|
211
|
+
) else {
|
|
212
|
+
return std::ptr::null();
|
|
213
|
+
};
|
|
214
|
+
cstring_return(bytes.len(), |out| {
|
|
215
|
+
out[..bytes.len()].copy_from_slice(&bytes);
|
|
216
|
+
Some(bytes.len())
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Write an HTTP-date (`Sun, 06 Nov 1994 08:49:37 GMT`) into `out`. Returns
|
|
221
|
+
/// bytes written (29), or 0 on a too-small buffer / out-of-range year (use the
|
|
222
|
+
/// allocating `httpDate` napi path for that fallback). Fixed 32-byte stack
|
|
223
|
+
/// buffer core — the FFI sibling of the napi `httpDateInto` (kills the napi
|
|
224
|
+
/// crossing on the hot `httpDateInto` path).
|
|
225
|
+
///
|
|
226
|
+
/// # Safety
|
|
227
|
+
/// `out` must be valid for writes of up to `out_cap` bytes.
|
|
228
|
+
#[no_mangle]
|
|
229
|
+
pub unsafe extern "C" fn castrum_http_date_into(secs: f64, out: *mut u8, out_cap: usize) -> usize {
|
|
230
|
+
if out.is_null() {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
let output = slice::from_raw_parts_mut(out, out_cap);
|
|
234
|
+
crate::http::http_date::http_date_into_slice(secs as i64, output).unwrap_or(0)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Parse an IMF-fixdate back to unix seconds → packed `[u8 ok][i64 secs LE]`
|
|
238
|
+
/// (9 bytes; ok=0 → invalid). Mirrors the `castrum_json_sum_ids` ok-byte
|
|
239
|
+
/// convention so a legit epoch (`0`) is distinct from "invalid". Needed-size
|
|
240
|
+
/// convention: `0` = buffer too small (real error).
|
|
241
|
+
///
|
|
242
|
+
/// # Safety
|
|
243
|
+
/// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
|
|
244
|
+
#[no_mangle]
|
|
245
|
+
pub unsafe extern "C" fn castrum_parse_http_date(
|
|
246
|
+
data: *const u8,
|
|
247
|
+
len: usize,
|
|
248
|
+
out: *mut u8,
|
|
249
|
+
out_cap: usize,
|
|
250
|
+
) -> usize {
|
|
251
|
+
if data.is_null() || out.is_null() {
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
let input = slice::from_raw_parts(data, len);
|
|
255
|
+
let secs = panic_guard(|| crate::http::http_date::parse_http_date_secs(input), None);
|
|
256
|
+
match secs {
|
|
257
|
+
Some(secs) => {
|
|
258
|
+
if out_cap < 9 {
|
|
259
|
+
return 9;
|
|
260
|
+
}
|
|
261
|
+
let o = slice::from_raw_parts_mut(out, 9);
|
|
262
|
+
o[0] = 1;
|
|
263
|
+
o[1..9].copy_from_slice(&secs.to_le_bytes());
|
|
264
|
+
9
|
|
265
|
+
}
|
|
266
|
+
None => {
|
|
267
|
+
if out_cap < 1 {
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
slice::from_raw_parts_mut(out, 1)[0] = 0;
|
|
271
|
+
1
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Parse a `Content-Type` header into a packed verdict:
|
|
277
|
+
/// `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)][charset]
|
|
278
|
+
/// [u32 boundaryLen (0xFFFFFFFF = none)][boundary][u32 paramCount]{[u32 keyLen]
|
|
279
|
+
/// [key][u32 valLen][val]}`. Needed-size convention: `0` = invalid media type
|
|
280
|
+
/// (real error → throw); `w > out_cap` = exact required size; else written.
|
|
281
|
+
///
|
|
282
|
+
/// # Safety
|
|
283
|
+
/// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
|
|
284
|
+
#[no_mangle]
|
|
285
|
+
pub unsafe extern "C" fn castrum_parse_media_type(
|
|
286
|
+
data: *const u8,
|
|
287
|
+
len: usize,
|
|
288
|
+
out: *mut u8,
|
|
289
|
+
out_cap: usize,
|
|
290
|
+
) -> usize {
|
|
291
|
+
if data.is_null() || out.is_null() {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
let input = slice::from_raw_parts(data, len);
|
|
295
|
+
let Some(parsed) = panic_guard(
|
|
296
|
+
|| crate::http::media_type::parse_media_type_core(input).ok(),
|
|
297
|
+
None,
|
|
298
|
+
) else {
|
|
299
|
+
return 0;
|
|
300
|
+
};
|
|
301
|
+
// Borrowed charset/boundary lookups (no String clones) + direct write into
|
|
302
|
+
// the caller's buffer — no `format!`, no intermediate Vec, no final copy.
|
|
303
|
+
let charset = parsed
|
|
304
|
+
.params
|
|
305
|
+
.iter()
|
|
306
|
+
.find(|(k, _)| k == "charset")
|
|
307
|
+
.map(|(_, v)| v.as_bytes());
|
|
308
|
+
let boundary = parsed
|
|
309
|
+
.params
|
|
310
|
+
.iter()
|
|
311
|
+
.find(|(k, _)| k == "boundary")
|
|
312
|
+
.map(|(_, v)| v.as_bytes());
|
|
313
|
+
let mt_len = parsed.ty.len() + 1 + parsed.subtype.len();
|
|
314
|
+
let mut needed = 4 + mt_len;
|
|
315
|
+
needed += charset.map_or(4, |v| 4 + v.len());
|
|
316
|
+
needed += boundary.map_or(4, |v| 4 + v.len());
|
|
317
|
+
needed += 4; // paramCount
|
|
318
|
+
for (k, v) in &parsed.params {
|
|
319
|
+
needed += 4 + k.len() + 4 + v.len();
|
|
320
|
+
}
|
|
321
|
+
if needed > out_cap {
|
|
322
|
+
return needed;
|
|
323
|
+
}
|
|
324
|
+
let out = slice::from_raw_parts_mut(out, needed);
|
|
325
|
+
let mut wp = 0usize;
|
|
326
|
+
out[wp..wp + 4].copy_from_slice(&(mt_len as u32).to_le_bytes());
|
|
327
|
+
wp += 4;
|
|
328
|
+
out[wp..wp + parsed.ty.len()].copy_from_slice(parsed.ty.as_bytes());
|
|
329
|
+
wp += parsed.ty.len();
|
|
330
|
+
out[wp] = b'/';
|
|
331
|
+
wp += 1;
|
|
332
|
+
out[wp..wp + parsed.subtype.len()].copy_from_slice(parsed.subtype.as_bytes());
|
|
333
|
+
wp += parsed.subtype.len();
|
|
334
|
+
// charset option slot ([u32 len][charset] or u32::MAX when absent)
|
|
335
|
+
match charset {
|
|
336
|
+
Some(v) => {
|
|
337
|
+
out[wp..wp + 4].copy_from_slice(&(v.len() as u32).to_le_bytes());
|
|
338
|
+
wp += 4;
|
|
339
|
+
out[wp..wp + v.len()].copy_from_slice(v);
|
|
340
|
+
wp += v.len();
|
|
341
|
+
}
|
|
342
|
+
None => {
|
|
343
|
+
out[wp..wp + 4].copy_from_slice(&u32::MAX.to_le_bytes());
|
|
344
|
+
wp += 4;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// boundary option slot
|
|
348
|
+
match boundary {
|
|
349
|
+
Some(v) => {
|
|
350
|
+
out[wp..wp + 4].copy_from_slice(&(v.len() as u32).to_le_bytes());
|
|
351
|
+
wp += 4;
|
|
352
|
+
out[wp..wp + v.len()].copy_from_slice(v);
|
|
353
|
+
wp += v.len();
|
|
354
|
+
}
|
|
355
|
+
None => {
|
|
356
|
+
out[wp..wp + 4].copy_from_slice(&u32::MAX.to_le_bytes());
|
|
357
|
+
wp += 4;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// params
|
|
361
|
+
out[wp..wp + 4].copy_from_slice(&(parsed.params.len() as u32).to_le_bytes());
|
|
362
|
+
wp += 4;
|
|
363
|
+
for (k, v) in &parsed.params {
|
|
364
|
+
out[wp..wp + 4].copy_from_slice(&(k.len() as u32).to_le_bytes());
|
|
365
|
+
wp += 4;
|
|
366
|
+
out[wp..wp + k.len()].copy_from_slice(k.as_bytes());
|
|
367
|
+
wp += k.len();
|
|
368
|
+
out[wp..wp + 4].copy_from_slice(&(v.len() as u32).to_le_bytes());
|
|
369
|
+
wp += 4;
|
|
370
|
+
out[wp..wp + v.len()].copy_from_slice(v.as_bytes());
|
|
371
|
+
wp += v.len();
|
|
372
|
+
}
|
|
373
|
+
debug_assert_eq!(wp, needed);
|
|
374
|
+
needed
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// Parse an Accept-Encoding header into a packed verdict:
|
|
378
|
+
/// `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}` (empty header → count 0,
|
|
379
|
+
/// 4 bytes). Needed-size convention: `0` = buffer too small (real error);
|
|
380
|
+
/// `w > out_cap` = exact required size; else bytes written.
|
|
381
|
+
///
|
|
382
|
+
/// # Safety
|
|
383
|
+
/// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
|
|
384
|
+
#[no_mangle]
|
|
385
|
+
pub unsafe extern "C" fn castrum_parse_accept_encoding(
|
|
386
|
+
data: *const u8,
|
|
387
|
+
len: usize,
|
|
388
|
+
out: *mut u8,
|
|
389
|
+
out_cap: usize,
|
|
390
|
+
) -> usize {
|
|
391
|
+
if data.is_null() || out.is_null() {
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
let input = slice::from_raw_parts(data, len);
|
|
395
|
+
let Some(prefs) = panic_guard(
|
|
396
|
+
|| Some(crate::http::accept::parse_accept_encoding_core(input)),
|
|
397
|
+
None,
|
|
398
|
+
) else {
|
|
399
|
+
return 0;
|
|
400
|
+
};
|
|
401
|
+
// Write the packed [u32 count]{[u32 encLen][enc][f32 q][u32 order]} verdict
|
|
402
|
+
// directly into the caller's buffer — no intermediate Vec + copy.
|
|
403
|
+
let mut needed = 4usize;
|
|
404
|
+
for p in &prefs {
|
|
405
|
+
needed += 4 + p.encoding.len() + 4 + 4;
|
|
406
|
+
}
|
|
407
|
+
if needed > out_cap {
|
|
408
|
+
return needed;
|
|
409
|
+
}
|
|
410
|
+
let out = slice::from_raw_parts_mut(out, needed);
|
|
411
|
+
let mut wp = 4usize;
|
|
412
|
+
out[0..4].copy_from_slice(&(prefs.len() as u32).to_le_bytes());
|
|
413
|
+
for p in &prefs {
|
|
414
|
+
out[wp..wp + 4].copy_from_slice(&(p.encoding.len() as u32).to_le_bytes());
|
|
415
|
+
wp += 4;
|
|
416
|
+
out[wp..wp + p.encoding.len()].copy_from_slice(p.encoding.as_bytes());
|
|
417
|
+
wp += p.encoding.len();
|
|
418
|
+
out[wp..wp + 4].copy_from_slice(&p.q.to_le_bytes());
|
|
419
|
+
wp += 4;
|
|
420
|
+
out[wp..wp + 4].copy_from_slice(&p.order.to_le_bytes());
|
|
421
|
+
wp += 4;
|
|
422
|
+
}
|
|
423
|
+
debug_assert_eq!(wp, needed);
|
|
424
|
+
wp
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Percent-encode a query string from a packed `[u32 count]{[u32 keyLen][key]
|
|
428
|
+
/// [u32 valLen][val]}` input (the JS `packPairs` layout) → cstring, keys
|
|
429
|
+
/// SORTED (matches the napi `BTreeMap` ordering). Returns `null` on malformed
|
|
430
|
+
/// packed input / non-UTF-8 (napi parity: throws).
|
|
431
|
+
///
|
|
432
|
+
/// # Safety
|
|
433
|
+
/// `data` must be valid for reads of `len` bytes.
|
|
434
|
+
#[no_mangle]
|
|
435
|
+
pub unsafe extern "C" fn castrum_url_encode_query(
|
|
436
|
+
data: *const u8,
|
|
437
|
+
len: usize,
|
|
438
|
+
) -> *const std::os::raw::c_char {
|
|
439
|
+
if data.is_null() {
|
|
440
|
+
return std::ptr::null();
|
|
441
|
+
}
|
|
442
|
+
let input = slice::from_raw_parts(data, len);
|
|
443
|
+
let Some(s) = panic_guard(
|
|
444
|
+
|| {
|
|
445
|
+
if input.len() < 4 {
|
|
446
|
+
return None;
|
|
447
|
+
}
|
|
448
|
+
let count = u32::from_le_bytes([input[0], input[1], input[2], input[3]]) as usize;
|
|
449
|
+
let mut map = std::collections::BTreeMap::new();
|
|
450
|
+
let mut off = 4usize;
|
|
451
|
+
for _ in 0..count {
|
|
452
|
+
if off + 4 > input.len() {
|
|
453
|
+
return None;
|
|
454
|
+
}
|
|
455
|
+
let klen = u32::from_le_bytes([
|
|
456
|
+
input[off],
|
|
457
|
+
input[off + 1],
|
|
458
|
+
input[off + 2],
|
|
459
|
+
input[off + 3],
|
|
460
|
+
]) as usize;
|
|
461
|
+
off += 4;
|
|
462
|
+
if off + klen > input.len() {
|
|
463
|
+
return None;
|
|
464
|
+
}
|
|
465
|
+
let key = std::str::from_utf8(&input[off..off + klen])
|
|
466
|
+
.ok()?
|
|
467
|
+
.to_string();
|
|
468
|
+
off += klen;
|
|
469
|
+
if off + 4 > input.len() {
|
|
470
|
+
return None;
|
|
471
|
+
}
|
|
472
|
+
let vlen = u32::from_le_bytes([
|
|
473
|
+
input[off],
|
|
474
|
+
input[off + 1],
|
|
475
|
+
input[off + 2],
|
|
476
|
+
input[off + 3],
|
|
477
|
+
]) as usize;
|
|
478
|
+
off += 4;
|
|
479
|
+
if off + vlen > input.len() {
|
|
480
|
+
return None;
|
|
481
|
+
}
|
|
482
|
+
let val = std::str::from_utf8(&input[off..off + vlen])
|
|
483
|
+
.ok()?
|
|
484
|
+
.to_string();
|
|
485
|
+
off += vlen;
|
|
486
|
+
map.insert(key, val);
|
|
487
|
+
}
|
|
488
|
+
let mut out = Vec::new();
|
|
489
|
+
let mut scratch = Vec::new();
|
|
490
|
+
for (i, (k, v)) in map.iter().enumerate() {
|
|
491
|
+
if i > 0 {
|
|
492
|
+
out.push(b'&');
|
|
493
|
+
}
|
|
494
|
+
crate::http::url_join::encode_query_component(k.as_bytes(), &mut scratch, &mut out)
|
|
495
|
+
.ok()?;
|
|
496
|
+
out.push(b'=');
|
|
497
|
+
crate::http::url_join::encode_query_component(v.as_bytes(), &mut scratch, &mut out)
|
|
498
|
+
.ok()?;
|
|
499
|
+
}
|
|
500
|
+
String::from_utf8(out).ok()
|
|
501
|
+
},
|
|
502
|
+
None,
|
|
503
|
+
) else {
|
|
504
|
+
return std::ptr::null();
|
|
505
|
+
};
|
|
506
|
+
cstring_return(s.len(), |out| {
|
|
507
|
+
out[..s.len()].copy_from_slice(s.as_bytes());
|
|
508
|
+
Some(s.len())
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/// RFC 3986 URL resolution → cstring (base + reference). Returns `null` on
|
|
513
|
+
/// non-UTF-8 input (napi parity: throws). Mirrors the napi `url_resolve`.
|
|
514
|
+
///
|
|
515
|
+
/// # Safety
|
|
516
|
+
/// `base`/`reference` must be valid for reads of their lengths.
|
|
517
|
+
#[no_mangle]
|
|
518
|
+
pub unsafe extern "C" fn castrum_url_resolve(
|
|
519
|
+
base: *const u8,
|
|
520
|
+
blen: usize,
|
|
521
|
+
reference: *const u8,
|
|
522
|
+
rlen: usize,
|
|
523
|
+
) -> *const std::os::raw::c_char {
|
|
524
|
+
if base.is_null() || reference.is_null() {
|
|
525
|
+
return std::ptr::null();
|
|
526
|
+
}
|
|
527
|
+
let b = slice::from_raw_parts(base, blen);
|
|
528
|
+
let r = slice::from_raw_parts(reference, rlen);
|
|
529
|
+
let Some(s) = panic_guard(
|
|
530
|
+
|| {
|
|
531
|
+
let bs = std::str::from_utf8(b).ok()?;
|
|
532
|
+
let rs = std::str::from_utf8(r).ok()?;
|
|
533
|
+
Some(crate::http::url_join::recompose(
|
|
534
|
+
&crate::http::url_join::resolve_target(
|
|
535
|
+
&crate::http::url_join::parse_ref(bs),
|
|
536
|
+
&crate::http::url_join::parse_ref(rs),
|
|
537
|
+
),
|
|
538
|
+
))
|
|
539
|
+
},
|
|
540
|
+
None,
|
|
541
|
+
) else {
|
|
542
|
+
return std::ptr::null();
|
|
543
|
+
};
|
|
544
|
+
cstring_return(s.len(), |out| {
|
|
545
|
+
out[..s.len()].copy_from_slice(s.as_bytes());
|
|
546
|
+
Some(s.len())
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/// Resolve a reference against a `UrlBuilder`'s PRECOMPILED base via its opaque
|
|
551
|
+
/// inner handle (from the napi `inner_ptr()`). Returns bytes written (0 = null
|
|
552
|
+
/// handle / non-UTF-8 reference / panic → real error); a result larger than
|
|
553
|
+
/// `out_cap` reports the exact needed size so the caller retries once
|
|
554
|
+
/// (growExact).
|
|
555
|
+
///
|
|
556
|
+
/// # Safety
|
|
557
|
+
/// `inner` must be a valid `UrlBuilder` pointer from `inner_ptr()` and must
|
|
558
|
+
/// stay alive for the call (the JS wrapper holds the napi instance).
|
|
559
|
+
/// `reference` must be valid for reads of `reference_len`; `out` for
|
|
560
|
+
/// `out_cap` writes.
|
|
561
|
+
#[no_mangle]
|
|
562
|
+
pub unsafe extern "C" fn castrum_url_builder_resolve(
|
|
563
|
+
inner: usize,
|
|
564
|
+
reference: *const u8,
|
|
565
|
+
reference_len: usize,
|
|
566
|
+
out: *mut u8,
|
|
567
|
+
out_cap: usize,
|
|
568
|
+
) -> usize {
|
|
569
|
+
if inner == 0 || reference.is_null() || out.is_null() {
|
|
570
|
+
return 0;
|
|
571
|
+
}
|
|
572
|
+
let r = slice::from_raw_parts(reference, reference_len);
|
|
573
|
+
let Some(bytes) = panic_guard(
|
|
574
|
+
|| {
|
|
575
|
+
crate::http::url_join::url_builder_resolve_core(
|
|
576
|
+
inner as *const crate::http::url_join::UrlBuilder,
|
|
577
|
+
r,
|
|
578
|
+
)
|
|
579
|
+
.ok()
|
|
580
|
+
},
|
|
581
|
+
None,
|
|
582
|
+
) else {
|
|
583
|
+
return 0;
|
|
584
|
+
};
|
|
585
|
+
if bytes.len() > out_cap {
|
|
586
|
+
return bytes.len();
|
|
587
|
+
}
|
|
588
|
+
slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(&bytes);
|
|
589
|
+
bytes.len()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/// Extension → MIME type → cstring (unknown → `application/octet-stream`).
|
|
593
|
+
/// Never fails (the core's fallback is infallible) — `null` only on a null
|
|
594
|
+
/// pointer or a panic (programmer error).
|
|
595
|
+
///
|
|
596
|
+
/// `ext` is a `bun:ffi` `cstring` ARG — the engine transcodes the JS extension
|
|
597
|
+
/// in-engine (JS does zero encode; the callee borrows via `CStr::from_ptr`).
|
|
598
|
+
///
|
|
599
|
+
/// # Safety
|
|
600
|
+
/// `ext` must be a valid NUL-terminated C string.
|
|
601
|
+
#[no_mangle]
|
|
602
|
+
pub unsafe extern "C" fn castrum_mime_from_extension(
|
|
603
|
+
ext: *const std::os::raw::c_char,
|
|
604
|
+
) -> *const std::os::raw::c_char {
|
|
605
|
+
if ext.is_null() {
|
|
606
|
+
return std::ptr::null();
|
|
607
|
+
}
|
|
608
|
+
let input = std::ffi::CStr::from_ptr(ext).to_bytes();
|
|
609
|
+
let mime = panic_guard(
|
|
610
|
+
|| crate::http::mime_lookup::mime_from_extension_bytes(input),
|
|
611
|
+
b"application/octet-stream".to_vec(),
|
|
612
|
+
);
|
|
613
|
+
cstring_return(mime.len(), |out| {
|
|
614
|
+
out[..mime.len()].copy_from_slice(&mime);
|
|
615
|
+
Some(mime.len())
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/// HTTP request parse → packed output into `out`.
|
|
620
|
+
///
|
|
621
|
+
/// # Safety
|
|
622
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
623
|
+
#[no_mangle]
|
|
624
|
+
pub unsafe extern "C" fn castrum_http_parse_request_packed(
|
|
625
|
+
data: *const u8,
|
|
626
|
+
len: usize,
|
|
627
|
+
out: *mut u8,
|
|
628
|
+
out_cap: usize,
|
|
629
|
+
) -> usize {
|
|
630
|
+
if data.is_null() || out.is_null() {
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
crate::http::http_parser::http_parse_request_packed_into_slice(
|
|
634
|
+
slice::from_raw_parts(data, len),
|
|
635
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
636
|
+
)
|
|
637
|
+
.unwrap_or(0)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/// Query string parse → packed output into `out`.
|
|
641
|
+
///
|
|
642
|
+
/// # Convention (needed-size)
|
|
643
|
+
/// The single-pass writer runs first; on a too-small buffer the exact-size pass
|
|
644
|
+
/// runs (rare) and returns the EXACT required size, so the JS wrapper allocates
|
|
645
|
+
/// ONCE and retries — no 9× pre-size, no re-run loop. `0` remains a REAL error
|
|
646
|
+
/// (malformed `%XX`).
|
|
647
|
+
///
|
|
648
|
+
/// # Safety
|
|
649
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
650
|
+
#[no_mangle]
|
|
651
|
+
pub unsafe extern "C" fn castrum_query_parse_packed(
|
|
652
|
+
data: *const u8,
|
|
653
|
+
len: usize,
|
|
654
|
+
out: *mut u8,
|
|
655
|
+
out_cap: usize,
|
|
656
|
+
) -> usize {
|
|
657
|
+
if data.is_null() || out.is_null() {
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
let input = slice::from_raw_parts(data, len);
|
|
661
|
+
let output = slice::from_raw_parts_mut(out, out_cap);
|
|
662
|
+
match crate::http::query_parser::query_parse_packed_into_slice(input, output) {
|
|
663
|
+
Ok(w) => w,
|
|
664
|
+
// Too-small OR malformed — the size pass disambiguates: it parses the
|
|
665
|
+
// same input, so Ok(needed) ⇒ too-small, Err ⇒ malformed (real error).
|
|
666
|
+
Err(_) => crate::http::query_parser::query_parse_packed_size(input).unwrap_or(0),
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/// Cookie header parse → packed output into `out`.
|
|
671
|
+
///
|
|
672
|
+
/// Same needed-size convention as `castrum_query_parse_packed`.
|
|
673
|
+
///
|
|
674
|
+
/// # Safety
|
|
675
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
676
|
+
#[no_mangle]
|
|
677
|
+
pub unsafe extern "C" fn castrum_cookie_parse_packed(
|
|
678
|
+
data: *const u8,
|
|
679
|
+
len: usize,
|
|
680
|
+
out: *mut u8,
|
|
681
|
+
out_cap: usize,
|
|
682
|
+
) -> usize {
|
|
683
|
+
if data.is_null() || out.is_null() {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
let input = slice::from_raw_parts(data, len);
|
|
687
|
+
let output = slice::from_raw_parts_mut(out, out_cap);
|
|
688
|
+
match crate::http::cookie_parser::cookie_parse_packed_into_slice(input, output) {
|
|
689
|
+
Ok(w) => w,
|
|
690
|
+
Err(_) => crate::http::cookie_parser::cookie_parse_packed_size(input).unwrap_or(0),
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/// Parse an `application/x-www-form-urlencoded` body into packed pairs — the
|
|
695
|
+
/// x-www-form-urlencoded wire format is identical to the query parser's core
|
|
696
|
+
/// (`query_parse_packed_into_slice`), so this is a thin alias.
|
|
697
|
+
///
|
|
698
|
+
/// # Safety
|
|
699
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
700
|
+
#[no_mangle]
|
|
701
|
+
pub unsafe extern "C" fn castrum_form_parse_packed(
|
|
702
|
+
data: *const u8,
|
|
703
|
+
len: usize,
|
|
704
|
+
out: *mut u8,
|
|
705
|
+
out_cap: usize,
|
|
706
|
+
) -> usize {
|
|
707
|
+
castrum_query_parse_packed(data, len, out, out_cap)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/// Parse a `multipart/form-data` body into the packed parts layout (the same
|
|
711
|
+
/// `[u32 count]{[u32 name_len][name][...]}` wire format as the batch API).
|
|
712
|
+
/// Returns bytes written (0 on malformed input); a result larger than `out_cap`
|
|
713
|
+
/// reports the exact needed size so the caller can retry once (see
|
|
714
|
+
/// `compress_to_out!` for the convention).
|
|
715
|
+
///
|
|
716
|
+
/// # Safety
|
|
717
|
+
/// `body`/`boundary` must be valid for reads of their lengths; `out` for
|
|
718
|
+
/// writes up to `out_cap`.
|
|
719
|
+
#[no_mangle]
|
|
720
|
+
pub unsafe extern "C" fn castrum_multipart_parse_packed(
|
|
721
|
+
body: *const u8,
|
|
722
|
+
blen: usize,
|
|
723
|
+
boundary: *const u8,
|
|
724
|
+
boundary_len: usize,
|
|
725
|
+
out: *mut u8,
|
|
726
|
+
out_cap: usize,
|
|
727
|
+
) -> usize {
|
|
728
|
+
if body.is_null() || boundary.is_null() || out.is_null() {
|
|
729
|
+
return 0;
|
|
730
|
+
}
|
|
731
|
+
// Wrap in panic_guard: the parser allocates internally — a panic must not
|
|
732
|
+
// unwind through the C ABI (process crash); it becomes 0 instead.
|
|
733
|
+
let packed = panic_guard(
|
|
734
|
+
|| {
|
|
735
|
+
let parts = crate::http::multipart::parse_multipart_limited(
|
|
736
|
+
slice::from_raw_parts(body, blen),
|
|
737
|
+
slice::from_raw_parts(boundary, boundary_len),
|
|
738
|
+
&Default::default(),
|
|
739
|
+
);
|
|
740
|
+
let mut buf = Vec::new();
|
|
741
|
+
crate::http::multipart::parts_to_packed(&parts, &mut buf);
|
|
742
|
+
buf
|
|
743
|
+
},
|
|
744
|
+
Vec::new(),
|
|
745
|
+
);
|
|
746
|
+
if packed.len() > out_cap {
|
|
747
|
+
// Needed-size convention (see compress_to_out!): exact retry, no re-run.
|
|
748
|
+
return packed.len();
|
|
749
|
+
}
|
|
750
|
+
slice::from_raw_parts_mut(out, packed.len()).copy_from_slice(&packed);
|
|
751
|
+
packed.len()
|
|
752
|
+
}
|