castrum 0.9.3 → 0.9.6
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 +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- 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 +105 -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 +69 -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/task.rs +172 -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 +136 -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 +211 -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 +770 -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 +567 -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 +883 -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 +73 -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/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -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 +594 -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/health.ts +1 -2
- 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 +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- 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 +71 -27
- 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/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// rust/http/headers.rs — HeaderRefs: zero-alloc packed header parser
|
|
2
|
+
// Uses the shared word-at-a-time comparator from `bytes.rs`.
|
|
3
|
+
//
|
|
4
|
+
// Pure core (no napi types): errors are plain `String` messages; the napi
|
|
5
|
+
// boundary maps them to JS errors.
|
|
6
|
+
|
|
7
|
+
use crate::util::bytes::ascii_eq_ignore_case;
|
|
8
|
+
|
|
9
|
+
// ── Header presence flags ─────────────────────────────────────────
|
|
10
|
+
const HAS_ORIGIN: u8 = 1 << 0;
|
|
11
|
+
const HAS_XFF: u8 = 1 << 1;
|
|
12
|
+
const HAS_X_REAL_IP: u8 = 1 << 2;
|
|
13
|
+
const HAS_XFP: u8 = 1 << 3;
|
|
14
|
+
const HAS_ACRM: u8 = 1 << 4;
|
|
15
|
+
|
|
16
|
+
/// Zero-alloc reference-based representation of interesting headers.
|
|
17
|
+
#[derive(Clone, Copy)]
|
|
18
|
+
pub struct HeaderRefs<'a> {
|
|
19
|
+
origin: Option<&'a [u8]>,
|
|
20
|
+
cookie: Option<&'a [u8]>,
|
|
21
|
+
xff: Option<&'a [u8]>,
|
|
22
|
+
x_real_ip: Option<&'a [u8]>,
|
|
23
|
+
x_forwarded_proto: Option<&'a [u8]>,
|
|
24
|
+
acrm: Option<&'a [u8]>,
|
|
25
|
+
acrh: Option<&'a [u8]>,
|
|
26
|
+
flags: u8,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl<'a> HeaderRefs<'a> {
|
|
30
|
+
#[inline(always)]
|
|
31
|
+
pub fn empty() -> Self {
|
|
32
|
+
Self {
|
|
33
|
+
origin: None,
|
|
34
|
+
cookie: None,
|
|
35
|
+
xff: None,
|
|
36
|
+
x_real_ip: None,
|
|
37
|
+
x_forwarded_proto: None,
|
|
38
|
+
acrm: None,
|
|
39
|
+
acrh: None,
|
|
40
|
+
flags: 0,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Parse packed headers from the input buffer.
|
|
45
|
+
/// Format: [u16 count] repeated { [u16 name_len] [name] [u32 value_len] [value] }
|
|
46
|
+
#[inline]
|
|
47
|
+
pub fn parse(
|
|
48
|
+
packed: &'a [u8],
|
|
49
|
+
is_options: bool,
|
|
50
|
+
max_headers: usize,
|
|
51
|
+
) -> std::result::Result<Self, String> {
|
|
52
|
+
let mut h = Self::empty();
|
|
53
|
+
|
|
54
|
+
if packed.len() < 2 {
|
|
55
|
+
return Ok(h);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let count = u16::from_le_bytes([packed[0], packed[1]]) as usize;
|
|
59
|
+
if count > max_headers {
|
|
60
|
+
return Err("too many headers".to_string());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let mut pos = 2usize;
|
|
64
|
+
|
|
65
|
+
for _ in 0..count {
|
|
66
|
+
if pos + 2 > packed.len() {
|
|
67
|
+
return Err("malformed packed headers".to_string());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let name_len = u16::from_le_bytes([packed[pos], packed[pos + 1]]) as usize;
|
|
71
|
+
pos += 2;
|
|
72
|
+
|
|
73
|
+
if pos + name_len > packed.len() {
|
|
74
|
+
return Err("malformed packed header name".to_string());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let name = &packed[pos..pos + name_len];
|
|
78
|
+
pos += name_len;
|
|
79
|
+
|
|
80
|
+
if pos + 4 > packed.len() {
|
|
81
|
+
return Err("malformed packed header value length".to_string());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let value_len = u32::from_le_bytes([
|
|
85
|
+
packed[pos],
|
|
86
|
+
packed[pos + 1],
|
|
87
|
+
packed[pos + 2],
|
|
88
|
+
packed[pos + 3],
|
|
89
|
+
]) as usize;
|
|
90
|
+
pos += 4;
|
|
91
|
+
|
|
92
|
+
if pos + value_len > packed.len() {
|
|
93
|
+
return Err("malformed packed header value".to_string());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let value = &packed[pos..pos + value_len];
|
|
97
|
+
pos += value_len;
|
|
98
|
+
|
|
99
|
+
if name.is_empty() {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let first = name[0] | 0x20;
|
|
104
|
+
|
|
105
|
+
// Fast path: dispatch based on first character + name length
|
|
106
|
+
if first == b'o' && name.len() == 6 {
|
|
107
|
+
if ascii_eq_ignore_case(name, b"origin") {
|
|
108
|
+
h.flags |= HAS_ORIGIN;
|
|
109
|
+
h.origin = Some(value);
|
|
110
|
+
}
|
|
111
|
+
} else if first == b'c' && name.len() == 6 {
|
|
112
|
+
if ascii_eq_ignore_case(name, b"cookie") {
|
|
113
|
+
h.cookie = Some(value);
|
|
114
|
+
}
|
|
115
|
+
} else if first == b'x' {
|
|
116
|
+
let name_len = name.len();
|
|
117
|
+
if name_len == 15 && ascii_eq_ignore_case(name, b"x-forwarded-for") {
|
|
118
|
+
h.flags |= HAS_XFF;
|
|
119
|
+
h.xff = Some(value);
|
|
120
|
+
} else if name_len == 9 && ascii_eq_ignore_case(name, b"x-real-ip") {
|
|
121
|
+
h.flags |= HAS_X_REAL_IP;
|
|
122
|
+
h.x_real_ip = Some(value);
|
|
123
|
+
} else if name_len == 17 && ascii_eq_ignore_case(name, b"x-forwarded-proto") {
|
|
124
|
+
h.flags |= HAS_XFP;
|
|
125
|
+
h.x_forwarded_proto = Some(value);
|
|
126
|
+
}
|
|
127
|
+
} else if first == b'a' && is_options {
|
|
128
|
+
if name.len() == 29 && ascii_eq_ignore_case(name, b"access-control-request-method")
|
|
129
|
+
{
|
|
130
|
+
h.flags |= HAS_ACRM;
|
|
131
|
+
h.acrm = Some(value);
|
|
132
|
+
} else if name.len() == 30
|
|
133
|
+
&& ascii_eq_ignore_case(name, b"access-control-request-headers")
|
|
134
|
+
{
|
|
135
|
+
h.acrh = Some(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
Ok(h)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[inline(always)]
|
|
144
|
+
pub fn origin(&self) -> Option<&[u8]> {
|
|
145
|
+
self.origin
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[inline(always)]
|
|
149
|
+
pub fn cookie(&self) -> Option<&[u8]> {
|
|
150
|
+
self.cookie
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[inline(always)]
|
|
154
|
+
pub fn xff(&self) -> Option<&[u8]> {
|
|
155
|
+
self.xff
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[inline(always)]
|
|
159
|
+
pub fn x_real_ip(&self) -> Option<&[u8]> {
|
|
160
|
+
self.x_real_ip
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[inline(always)]
|
|
164
|
+
pub fn x_forwarded_proto(&self) -> Option<&[u8]> {
|
|
165
|
+
self.x_forwarded_proto
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[inline(always)]
|
|
169
|
+
pub fn acrm(&self) -> Option<&[u8]> {
|
|
170
|
+
self.acrm
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[inline(always)]
|
|
174
|
+
pub fn acrh(&self) -> Option<&[u8]> {
|
|
175
|
+
self.acrh
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[inline(always)]
|
|
179
|
+
pub fn has_origin(&self) -> bool {
|
|
180
|
+
(self.flags & HAS_ORIGIN) != 0
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[inline(always)]
|
|
184
|
+
pub fn has_xfp(&self) -> bool {
|
|
185
|
+
(self.flags & HAS_XFP) != 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[inline(always)]
|
|
189
|
+
pub fn has_acrm(&self) -> bool {
|
|
190
|
+
(self.flags & HAS_ACRM) != 0
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[cfg(test)]
|
|
195
|
+
mod tests {
|
|
196
|
+
use super::HeaderRefs;
|
|
197
|
+
use crate::test_support::pack_headers;
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn headers_parse_empty_ok() {
|
|
201
|
+
let h = HeaderRefs::parse(b"", false, 100).unwrap();
|
|
202
|
+
assert!(h.origin().is_none());
|
|
203
|
+
assert!(h.cookie().is_none());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn headers_parse_presence_and_values() {
|
|
208
|
+
let packed = pack_headers([
|
|
209
|
+
("Origin", "https://example.com"),
|
|
210
|
+
("Cookie", "session=abc"),
|
|
211
|
+
("X-Forwarded-For", "1.2.3.4"),
|
|
212
|
+
]);
|
|
213
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
214
|
+
assert_eq!(h.origin(), Some(&b"https://example.com"[..]));
|
|
215
|
+
assert_eq!(h.cookie(), Some(&b"session=abc"[..]));
|
|
216
|
+
assert_eq!(h.xff(), Some(&b"1.2.3.4"[..]));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn headers_parse_case_insensitive_names() {
|
|
221
|
+
let packed = pack_headers([("ORIGIN", "https://x.io")]);
|
|
222
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
223
|
+
assert_eq!(h.origin(), Some(&b"https://x.io"[..]));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn headers_parse_acrm_only_for_options() {
|
|
228
|
+
let packed = pack_headers([
|
|
229
|
+
("access-control-request-method", "POST"),
|
|
230
|
+
("access-control-request-headers", "Content-Type"),
|
|
231
|
+
]);
|
|
232
|
+
// Non-OPTIONS: ACRM/ACRH should not be captured.
|
|
233
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
234
|
+
assert!(!h.has_acrm());
|
|
235
|
+
// OPTIONS: they should be captured.
|
|
236
|
+
let h2 = HeaderRefs::parse(&packed, true, 100).unwrap();
|
|
237
|
+
assert!(h2.has_acrm());
|
|
238
|
+
// ACRH value is captured on OPTIONS (no dedicated flag/method).
|
|
239
|
+
assert_eq!(h2.acrh(), Some(&b"Content-Type"[..]));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn headers_parse_too_many_headers_rejected() {
|
|
244
|
+
let mut pairs: Vec<(String, String)> = Vec::new();
|
|
245
|
+
for i in 0..10 {
|
|
246
|
+
pairs.push(("x".to_string(), format!("v{i}")));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let mut packed = Vec::new();
|
|
250
|
+
packed.extend_from_slice(&(pairs.len() as u16).to_le_bytes());
|
|
251
|
+
for (name, value) in &pairs {
|
|
252
|
+
packed.extend_from_slice(&(name.len() as u16).to_le_bytes());
|
|
253
|
+
packed.extend_from_slice(name.as_bytes());
|
|
254
|
+
packed.extend_from_slice(&(value.len() as u32).to_le_bytes());
|
|
255
|
+
packed.extend_from_slice(value.as_bytes());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
assert!(HeaderRefs::parse(&packed, false, 5).is_err());
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn headers_parse_malformed_rejected() {
|
|
263
|
+
// Declares a name_len that runs past the end of the buffer.
|
|
264
|
+
let packed = [2u8, 0, 0xFF, 0xFF, b'x'];
|
|
265
|
+
assert!(HeaderRefs::parse(&packed, false, 100).is_err());
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// rust/http/http_date.rs — HTTP-date (IMF-fixdate) formatting + parsing.
|
|
2
|
+
//
|
|
3
|
+
// `Sun, 06 Nov 1994 08:49:37 GMT` → unix seconds and back, with a zero-alloc
|
|
4
|
+
// fixed-width writer for hot loops. The date arithmetic uses Howard Hinnant's
|
|
5
|
+
// days↔civil algorithms (epoch-correct). Extracted from `etag.rs` so the
|
|
6
|
+
// HTTP cache semantics file owns only ETags + conditional requests.
|
|
7
|
+
|
|
8
|
+
use napi::bindgen_prelude::*;
|
|
9
|
+
use napi_derive::napi;
|
|
10
|
+
|
|
11
|
+
const DAY_NAMES: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
12
|
+
const MONTH_NAMES: [&str; 12] = [
|
|
13
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/// Days since epoch → civil (year, month, day) — Hinnant's algorithm.
|
|
17
|
+
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
|
18
|
+
let z = z + 719_468;
|
|
19
|
+
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
|
20
|
+
let doe = (z - era * 146_097) as u64;
|
|
21
|
+
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
22
|
+
let y = yoe as i64 + era * 400;
|
|
23
|
+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
24
|
+
let mp = (5 * doy + 2) / 153;
|
|
25
|
+
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
|
26
|
+
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
|
|
27
|
+
let y = if m <= 2 { y + 1 } else { y };
|
|
28
|
+
(y, m, d)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Civil date → days since epoch — Hinnant's inverse algorithm.
|
|
32
|
+
fn days_from_civil(y: i64, m: u32, d: u32) -> Option<i64> {
|
|
33
|
+
if !(1..=12).contains(&m) || d == 0 || d > 31 {
|
|
34
|
+
return None;
|
|
35
|
+
}
|
|
36
|
+
let y = if m <= 2 { y - 1 } else { y };
|
|
37
|
+
let era = if y >= 0 { y } else { y - 399 } / 400;
|
|
38
|
+
let yoe = (y - era * 400) as u64;
|
|
39
|
+
let mp = ((m as i64 + 9) % 12) as u64;
|
|
40
|
+
let doy = (153 * mp + 2) / 5 + (d - 1) as u64;
|
|
41
|
+
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
42
|
+
Some(era * 146_097 + doe as i64 - 719_468)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Format `value` as a zero-padded decimal of exactly `width` digits into
|
|
46
|
+
/// `out`. `value` must fit in `width` digits.
|
|
47
|
+
#[inline]
|
|
48
|
+
fn write_padded(out: &mut [u8], mut value: u32, width: usize) {
|
|
49
|
+
for i in (0..width).rev() {
|
|
50
|
+
out[i] = b'0' + (value % 10) as u8;
|
|
51
|
+
value /= 10;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Parsed http-date components (computed once, shared by the allocating and
|
|
56
|
+
/// pooled-output formatters).
|
|
57
|
+
struct HttpDateParts {
|
|
58
|
+
y: i64,
|
|
59
|
+
m: u32,
|
|
60
|
+
d: u32,
|
|
61
|
+
hh: i64,
|
|
62
|
+
mm: i64,
|
|
63
|
+
ss: i64,
|
|
64
|
+
wd: usize,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn http_date_parts(secs: i64) -> HttpDateParts {
|
|
68
|
+
let days = secs.div_euclid(86_400);
|
|
69
|
+
let rem = secs.rem_euclid(86_400);
|
|
70
|
+
let hh = rem / 3600;
|
|
71
|
+
let mm = (rem % 3600) / 60;
|
|
72
|
+
let ss = rem % 60;
|
|
73
|
+
let (y, m, d) = civil_from_days(days);
|
|
74
|
+
// 1970-01-01 was a Thursday; weekday Mon=0 → (days+3) mod 7.
|
|
75
|
+
let wd = ((days.rem_euclid(7) + 3) % 7) as usize;
|
|
76
|
+
HttpDateParts {
|
|
77
|
+
y,
|
|
78
|
+
m,
|
|
79
|
+
d,
|
|
80
|
+
hh,
|
|
81
|
+
mm,
|
|
82
|
+
ss,
|
|
83
|
+
wd,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Write the fixed-width `ddd, dd mmm yyyy hh:mm:ss GMT` layout into `out`
|
|
88
|
+
/// (29 bytes, no heap allocation). `Err` only for years outside 0..=9999
|
|
89
|
+
/// (where the fixed-width layout is undefined) or a too-small buffer.
|
|
90
|
+
fn write_http_date_parts(
|
|
91
|
+
p: &HttpDateParts,
|
|
92
|
+
out: &mut [u8],
|
|
93
|
+
) -> std::result::Result<usize, &'static str> {
|
|
94
|
+
// 3 + 2 (", ") + 2 + 1 (" ") + 3 + 1 (" ") + 4 + 1 (" ") + 2 + 1 (":") + 2
|
|
95
|
+
// + 1 (":") + 2 + 1 (" ") + 3 = 29
|
|
96
|
+
if out.len() < 29 {
|
|
97
|
+
return Err("http-date: output buffer too small");
|
|
98
|
+
}
|
|
99
|
+
if !(0..=9999).contains(&p.y) {
|
|
100
|
+
return Err("http-date: year out of range for fixed-width layout");
|
|
101
|
+
}
|
|
102
|
+
let mut i = 0usize;
|
|
103
|
+
out[i..i + 3].copy_from_slice(DAY_NAMES[p.wd].as_bytes());
|
|
104
|
+
i += 3;
|
|
105
|
+
out[i] = b',';
|
|
106
|
+
i += 1;
|
|
107
|
+
out[i] = b' ';
|
|
108
|
+
i += 1;
|
|
109
|
+
write_padded(&mut out[i..i + 2], p.d, 2);
|
|
110
|
+
i += 2;
|
|
111
|
+
out[i] = b' ';
|
|
112
|
+
i += 1;
|
|
113
|
+
out[i..i + 3].copy_from_slice(MONTH_NAMES[(p.m - 1) as usize].as_bytes());
|
|
114
|
+
i += 3;
|
|
115
|
+
out[i] = b' ';
|
|
116
|
+
i += 1;
|
|
117
|
+
write_padded(&mut out[i..i + 4], p.y as u32, 4);
|
|
118
|
+
i += 4;
|
|
119
|
+
out[i] = b' ';
|
|
120
|
+
i += 1;
|
|
121
|
+
write_padded(&mut out[i..i + 2], p.hh as u32, 2);
|
|
122
|
+
i += 2;
|
|
123
|
+
out[i] = b':';
|
|
124
|
+
i += 1;
|
|
125
|
+
write_padded(&mut out[i..i + 2], p.mm as u32, 2);
|
|
126
|
+
i += 2;
|
|
127
|
+
out[i] = b':';
|
|
128
|
+
i += 1;
|
|
129
|
+
write_padded(&mut out[i..i + 2], p.ss as u32, 2);
|
|
130
|
+
i += 2;
|
|
131
|
+
out[i] = b' ';
|
|
132
|
+
i += 1;
|
|
133
|
+
out[i..i + 3].copy_from_slice(b"GMT");
|
|
134
|
+
i += 3;
|
|
135
|
+
Ok(i)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Format a unix timestamp (seconds) as `Sun, 06 Nov 1994 08:49:37 GMT`.
|
|
139
|
+
/// Written into a fixed 32-byte stack buffer — no `format!`/heap allocation.
|
|
140
|
+
pub fn http_date_from_secs(secs: i64) -> String {
|
|
141
|
+
let p = http_date_parts(secs);
|
|
142
|
+
let mut buf = [0u8; 32];
|
|
143
|
+
match write_http_date_parts(&p, &mut buf) {
|
|
144
|
+
Ok(n) => String::from_utf8(buf[..n].to_vec()).expect("http-date is ASCII"),
|
|
145
|
+
Err(_) => {
|
|
146
|
+
// Year outside 0..=9999: HTTP-date is only well-defined for
|
|
147
|
+
// 0000-9999, but keep the exact `format!` fallback so behavior
|
|
148
|
+
// never changes from before the refactor.
|
|
149
|
+
format!(
|
|
150
|
+
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
|
|
151
|
+
DAY_NAMES[p.wd],
|
|
152
|
+
p.d,
|
|
153
|
+
MONTH_NAMES[(p.m - 1) as usize],
|
|
154
|
+
p.y,
|
|
155
|
+
p.hh,
|
|
156
|
+
p.mm,
|
|
157
|
+
p.ss
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Write an http-date directly into `out` (fixed 29 bytes, no heap alloc).
|
|
164
|
+
/// Returns bytes written; errors on a too-small buffer or a year outside
|
|
165
|
+
/// 0..=9999 (use the allocating `http_date` for that fallback).
|
|
166
|
+
pub fn http_date_into_slice(secs: i64, out: &mut [u8]) -> Result<usize> {
|
|
167
|
+
let p = http_date_parts(secs);
|
|
168
|
+
write_http_date_parts(&p, out).map_err(|e| Error::from_reason(e.to_string()))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Parse `Sun, 06 Nov 1994 08:49:37 GMT` back to unix seconds.
|
|
172
|
+
pub fn parse_http_date_secs(input: &[u8]) -> Option<i64> {
|
|
173
|
+
let s = std::str::from_utf8(input).ok()?.trim();
|
|
174
|
+
let comma = s.find(',')?;
|
|
175
|
+
let rest = s[comma + 1..].trim();
|
|
176
|
+
let mut parts = rest.split_whitespace();
|
|
177
|
+
let day: u32 = parts.next()?.parse().ok()?;
|
|
178
|
+
let mon = parts.next()?;
|
|
179
|
+
let year: i64 = parts.next()?.parse().ok()?;
|
|
180
|
+
let time = parts.next()?;
|
|
181
|
+
let month = MONTH_NAMES
|
|
182
|
+
.iter()
|
|
183
|
+
.position(|m| m.eq_ignore_ascii_case(mon))?
|
|
184
|
+
+ 1;
|
|
185
|
+
let mut tp = time.split(':');
|
|
186
|
+
let hh: u32 = tp.next()?.parse().ok()?;
|
|
187
|
+
let mm: u32 = tp.next()?.parse().ok()?;
|
|
188
|
+
let ss: u32 = tp.next()?.parse().ok()?;
|
|
189
|
+
if hh > 23 || mm > 59 || ss > 60 {
|
|
190
|
+
return None;
|
|
191
|
+
}
|
|
192
|
+
let days = days_from_civil(year, month as u32, day)?;
|
|
193
|
+
Some(days * 86_400 + i64::from(hh) * 3600 + i64::from(mm) * 60 + i64::from(ss))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[napi]
|
|
197
|
+
pub fn http_date(secs: Option<f64>) -> Buffer {
|
|
198
|
+
let s = secs.map(|v| v as i64).unwrap_or(0);
|
|
199
|
+
Buffer::from(http_date_from_secs(s).into_bytes())
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Pooled-output variant: writes the http-date into `output` and returns bytes
|
|
203
|
+
/// written, so hot loops can reuse a buffer instead of allocating a fresh
|
|
204
|
+
/// Buffer per call. Errors if `output` is too small or the year is outside the
|
|
205
|
+
/// fixed-width range (use the allocating `http_date` for that fallback).
|
|
206
|
+
#[napi]
|
|
207
|
+
pub fn http_date_into(secs: Option<f64>, mut output: Uint8Array) -> Result<u32> {
|
|
208
|
+
let s = secs.map(|v| v as i64).unwrap_or(0);
|
|
209
|
+
// SAFETY: `http_date_into_slice` is capacity-checked (returns Err on a
|
|
210
|
+
// too-small buffer before writing), and no other reference to `output` is
|
|
211
|
+
// live while it runs.
|
|
212
|
+
let written = unsafe { http_date_into_slice(s, output.as_mut())? };
|
|
213
|
+
Ok(written as u32)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[napi]
|
|
217
|
+
pub fn parse_http_date(input: Uint8Array) -> Option<BigInt> {
|
|
218
|
+
parse_http_date_secs(input.as_ref()).map(BigInt::from)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[cfg(test)]
|
|
222
|
+
mod tests {
|
|
223
|
+
use super::*;
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn http_date_epoch() {
|
|
227
|
+
assert_eq!(http_date_from_secs(0), "Thu, 01 Jan 1970 00:00:00 GMT");
|
|
228
|
+
assert_eq!(
|
|
229
|
+
http_date_from_secs(978_307_200),
|
|
230
|
+
"Mon, 01 Jan 2001 00:00:00 GMT"
|
|
231
|
+
);
|
|
232
|
+
assert_eq!(
|
|
233
|
+
http_date_from_secs(784_111_777),
|
|
234
|
+
"Sun, 06 Nov 1994 08:49:37 GMT"
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn http_date_roundtrip() {
|
|
240
|
+
for secs in [0i64, 1, 946_684_800, 784_111_777, 1_700_000_000] {
|
|
241
|
+
let s = http_date_from_secs(secs);
|
|
242
|
+
assert_eq!(parse_http_date_secs(s.as_bytes()), Some(secs));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn http_date_into_matches_allocating() {
|
|
248
|
+
for secs in [
|
|
249
|
+
0i64,
|
|
250
|
+
1,
|
|
251
|
+
946_684_800,
|
|
252
|
+
784_111_777,
|
|
253
|
+
1_700_000_000,
|
|
254
|
+
-1,
|
|
255
|
+
-86_400,
|
|
256
|
+
] {
|
|
257
|
+
let expected = http_date_from_secs(secs);
|
|
258
|
+
let mut out = [0u8; 32];
|
|
259
|
+
let n = http_date_into_slice(secs, &mut out).unwrap();
|
|
260
|
+
assert_eq!(&out[..n], expected.as_bytes(), "secs: {secs}");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn http_date_into_rejects_small_buffer() {
|
|
266
|
+
let mut out = [0u8; 28];
|
|
267
|
+
assert!(http_date_into_slice(0, &mut out).is_err());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn http_date_into_out_of_range_year_falls_back() {
|
|
272
|
+
// Year 10000 (253_402_300_800s): the fixed-width pooled writer errors;
|
|
273
|
+
// the allocating path keeps the exact format! fallback.
|
|
274
|
+
let big = 253_402_300_800i64; // 10000-01-01T00:00:00Z
|
|
275
|
+
let mut out = [0u8; 32];
|
|
276
|
+
assert!(http_date_into_slice(big, &mut out).is_err());
|
|
277
|
+
assert!(http_date_from_secs(big).contains("01 Jan 10000"));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[test]
|
|
281
|
+
fn parse_http_date_rejects_malformed() {
|
|
282
|
+
assert!(parse_http_date_secs(b"not a date").is_none());
|
|
283
|
+
assert!(parse_http_date_secs(b"Sun, 99 Nov 1994 08:49:37 GMT").is_none());
|
|
284
|
+
assert!(parse_http_date_secs(b"Sun, 06 Nov 1994 25:49:37 GMT").is_none());
|
|
285
|
+
}
|
|
286
|
+
}
|