castrum 0.7.0 → 0.9.0

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.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
package/rust/cors.rs DELETED
@@ -1,263 +0,0 @@
1
- // rust/cors.rs — CORS engine extracted from ingress.rs
2
- // Pre-computes method bitmask for O(1) method matching
3
-
4
- use crate::headers::HeaderRefs;
5
- use crate::method::MethodKind;
6
- use crate::util::trim_ascii_whitespace;
7
- use napi::{Error, Result, Status};
8
- use napi_derive::napi;
9
-
10
- // ── CORS engine ───────────────────────────────────────────────────
11
- #[derive(Clone)]
12
- pub enum CorsMode {
13
- Disabled,
14
- Wildcard,
15
- Allowlist(Vec<Box<[u8]>>),
16
- }
17
-
18
- #[derive(Clone)]
19
- pub struct CorsEngine {
20
- mode: CorsMode,
21
- credentials: bool,
22
- methods_wildcard: bool,
23
- // Bitmask of allowed methods for O(1) lookup
24
- method_bits: u16,
25
- headers_wildcard: bool,
26
- headers: Vec<Box<[u8]>>,
27
- }
28
-
29
- pub struct CorsEvaluation {
30
- pub allowed: bool,
31
- pub preflight: bool,
32
- }
33
-
34
- impl CorsEngine {
35
- pub fn disabled() -> Self {
36
- Self {
37
- mode: CorsMode::Disabled,
38
- credentials: false,
39
- methods_wildcard: false,
40
- method_bits: 0,
41
- headers_wildcard: false,
42
- headers: Vec::new(),
43
- }
44
- }
45
-
46
- pub fn from_options(opts: Option<CorsOptions>) -> Result<Self> {
47
- let Some(opts) = opts else {
48
- return Ok(Self::disabled());
49
- };
50
-
51
- let credentials = opts.allow_credentials.unwrap_or(false);
52
-
53
- let mode = match opts.allow_origin {
54
- Some(list) if !list.is_empty() => {
55
- if list.iter().any(|o| o == "*") {
56
- if credentials {
57
- return Err(Error::new(
58
- Status::InvalidArg,
59
- "CORS allowCredentials cannot be used with allowOrigin ['*']",
60
- ));
61
- }
62
- CorsMode::Wildcard
63
- } else {
64
- CorsMode::Allowlist(
65
- list.iter()
66
- .map(|s| s.as_bytes().to_vec().into_boxed_slice())
67
- .collect(),
68
- )
69
- }
70
- }
71
- _ => {
72
- if credentials {
73
- return Err(Error::new(
74
- Status::InvalidArg,
75
- "CORS allowCredentials requires an explicit allowOrigin list",
76
- ));
77
- }
78
- CorsMode::Wildcard
79
- }
80
- };
81
-
82
- let (methods_wildcard, method_bits) = parse_allowed_methods_bitset(opts.allow_methods);
83
- let (headers_wildcard, headers) = parse_allowed_headers(opts.allow_headers);
84
-
85
- Ok(Self {
86
- mode,
87
- credentials,
88
- methods_wildcard,
89
- method_bits,
90
- headers_wildcard,
91
- headers,
92
- })
93
- }
94
-
95
- #[inline]
96
- pub fn evaluate(&self, method: MethodKind, headers: &HeaderRefs) -> CorsEvaluation {
97
- let origin = match headers.origin() {
98
- Some(o) => o,
99
- None => {
100
- return CorsEvaluation {
101
- allowed: false,
102
- preflight: false,
103
- }
104
- }
105
- };
106
-
107
- let preflight = method == MethodKind::Options && headers.has_acrm();
108
-
109
- let mut allowed = match &self.mode {
110
- CorsMode::Disabled => false,
111
- CorsMode::Wildcard => !self.credentials,
112
- CorsMode::Allowlist(list) => {
113
- let origin_trim = trim_ascii_whitespace(origin);
114
- list.iter().any(|allowed| &**allowed == origin_trim)
115
- }
116
- };
117
-
118
- if allowed {
119
- if preflight {
120
- allowed = self.preflight_method_allowed(headers.acrm())
121
- && self.headers_allowed(headers.acrh());
122
- } else {
123
- allowed = self.method_allowed_kind(method);
124
- }
125
- }
126
-
127
- CorsEvaluation { allowed, preflight }
128
- }
129
-
130
- #[inline(always)]
131
- fn method_allowed_kind(&self, kind: MethodKind) -> bool {
132
- if self.methods_wildcard {
133
- return true;
134
- }
135
-
136
- if self.method_bits == 0 {
137
- return matches!(kind, MethodKind::Get | MethodKind::Head | MethodKind::Post);
138
- }
139
-
140
- (self.method_bits & kind.bit()) != 0
141
- }
142
-
143
- #[inline]
144
- fn preflight_method_allowed(&self, acrm: Option<&[u8]>) -> bool {
145
- let Some(acrm) = acrm else {
146
- return false;
147
- };
148
-
149
- let trimmed = trim_ascii_whitespace(acrm);
150
- if trimmed.is_empty() {
151
- return false;
152
- }
153
-
154
- let kind = MethodKind::from_bytes_ignore_case(trimmed);
155
- self.method_allowed_kind(kind)
156
- }
157
-
158
- #[inline]
159
- fn headers_allowed(&self, acrh: Option<&[u8]>) -> bool {
160
- let Some(acrh) = acrh else {
161
- return true;
162
- };
163
-
164
- if self.headers_wildcard {
165
- return true;
166
- }
167
-
168
- for requested in acrh.split(|&b| b == b',') {
169
- let name = trim_ascii_whitespace(requested);
170
- if name.is_empty() {
171
- continue;
172
- }
173
-
174
- let lower = name.to_ascii_lowercase();
175
-
176
- if self.headers.is_empty() {
177
- if !is_cors_safelisted_request_header(lower.as_slice()) {
178
- return false;
179
- }
180
- } else if !self
181
- .headers
182
- .iter()
183
- .any(|allowed| allowed.as_ref() == lower.as_slice())
184
- {
185
- return false;
186
- }
187
- }
188
-
189
- true
190
- }
191
- }
192
-
193
- fn is_cors_safelisted_request_header(name: &[u8]) -> bool {
194
- matches!(
195
- name,
196
- b"accept" | b"accept-language" | b"content-language" | b"content-type"
197
- )
198
- }
199
-
200
- fn parse_allowed_methods_bitset(list: Option<Vec<String>>) -> (bool, u16) {
201
- let Some(list) = list else {
202
- return (false, 0);
203
- };
204
-
205
- let mut bits = 0u16;
206
-
207
- for raw in list {
208
- let trimmed = raw.trim();
209
-
210
- if trimmed == "*" {
211
- return (true, 0);
212
- }
213
-
214
- if trimmed.is_empty() {
215
- continue;
216
- }
217
-
218
- let upper = trimmed.to_ascii_uppercase();
219
- let kind = MethodKind::from_str(&upper);
220
-
221
- if kind != MethodKind::Other {
222
- bits |= kind.bit();
223
- }
224
- }
225
-
226
- (false, bits)
227
- }
228
-
229
- fn parse_allowed_headers(list: Option<Vec<String>>) -> (bool, Vec<Box<[u8]>>) {
230
- let Some(list) = list else {
231
- return (false, Vec::new());
232
- };
233
-
234
- let mut headers = Vec::new();
235
-
236
- for raw in list {
237
- let trimmed = raw.trim();
238
-
239
- if trimmed == "*" {
240
- return (true, Vec::new());
241
- }
242
-
243
- if trimmed.is_empty() {
244
- continue;
245
- }
246
-
247
- headers.push(trimmed.to_ascii_lowercase().into_bytes().into_boxed_slice());
248
- }
249
-
250
- (false, headers)
251
- }
252
-
253
- // ── NAPI option struct ───────────────────────────────────────────
254
- #[napi(object)]
255
- #[derive(Clone)]
256
- pub struct CorsOptions {
257
- pub allow_origin: Option<Vec<String>>,
258
- pub allow_methods: Option<Vec<String>>,
259
- pub allow_headers: Option<Vec<String>>,
260
- pub expose_headers: Option<Vec<String>>,
261
- pub allow_credentials: Option<bool>,
262
- pub max_age: Option<u32>,
263
- }
package/rust/hashing.rs DELETED
@@ -1,164 +0,0 @@
1
- use napi::bindgen_prelude::*;
2
- use napi_derive::napi;
3
-
4
- use crate::util::should_parallelize;
5
- use xxhash_rust::xxh3::{xxh3_64, xxh3_64_with_seed, Xxh3};
6
-
7
- pub const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
8
- pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
9
-
10
- // ── FNV-1a (byte-by-byte, correct per spec) ──────────────────────
11
-
12
- /// FNV-1a in the "continue" style. FNV-1a is inherently byte-serial,
13
- /// so we process each byte individually (this is the correct algorithm).
14
- #[inline]
15
- pub fn fnv1a64_continue(mut hash: u64, input: &[u8]) -> u64 {
16
- for &b in input {
17
- hash ^= u64::from(b);
18
- hash = hash.wrapping_mul(FNV_PRIME);
19
- }
20
- hash
21
- }
22
-
23
- /// FNV-1a on a full byte slice, with fast path for small inputs.
24
- #[inline]
25
- pub fn fnv1a64_bytes(input: &[u8]) -> u64 {
26
- fnv1a64_continue(FNV_OFFSET_BASIS, input)
27
- }
28
-
29
- // ── XXHash wrappers ────────────────────────────────────────────────
30
-
31
- #[inline]
32
- pub fn fast_hash_bytes(input: &[u8]) -> u64 {
33
- xxh3_64(input)
34
- }
35
-
36
- #[inline]
37
- pub fn fast_hash_seeded(input: &[u8], seed: u64) -> u64 {
38
- xxh3_64_with_seed(input, seed)
39
- }
40
-
41
- /// Compute a composite cache key from method+path+query.
42
- #[inline]
43
- pub fn fast_hash_cache_key(method: &[u8], path: &[u8], query: &[u8]) -> u64 {
44
- let mut h = Xxh3::with_seed(0x9E37_79B9_7F4A_7C15);
45
- h.update(method);
46
- h.update(&[0]);
47
- h.update(path);
48
- h.update(&[0]);
49
- h.update(query);
50
- h.digest()
51
- }
52
-
53
- // ── Cow helpers for lazy / cached hashing ──────────────────────────
54
-
55
- /// A lazy hash value that can be computed once and cached.
56
- #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
57
- pub struct LazyHash(u64);
58
-
59
- impl LazyHash {
60
- #[inline(always)]
61
- pub fn new(value: u64) -> Self {
62
- Self(value)
63
- }
64
-
65
- #[inline(always)]
66
- pub fn get(&self) -> u64 {
67
- self.0
68
- }
69
- }
70
-
71
- // ── Napi exports ───────────────────────────────────────────────────
72
-
73
- #[napi]
74
- pub fn crc32(input: Uint8Array) -> u32 {
75
- crc32fast::hash(input.as_ref())
76
- }
77
-
78
- #[napi(js_name = "fnv1a64")]
79
- pub fn fnv1a64(input: Uint8Array) -> u64 {
80
- fnv1a64_bytes(input.as_ref())
81
- }
82
-
83
- /// CRC32 batch — zero-alloc inline iteration (no Vec allocation from unpack).
84
- #[napi]
85
- pub fn crc32_batch_packed(input: Uint8Array) -> Result<Buffer> {
86
- let data = input.as_ref();
87
- if data.len() < 4 {
88
- return Err(Error::from_reason("packed buffer: missing count"));
89
- }
90
-
91
- let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
92
-
93
- // Validate count early
94
- let max_possible = (data.len() - 4) / 4;
95
- if count > max_possible {
96
- return Err(Error::from_reason("packed buffer: impossible item count"));
97
- }
98
-
99
- let mut out = Vec::with_capacity(4 + count * 4);
100
- out.extend_from_slice(&(count as u32).to_le_bytes());
101
- out.resize(4 + count * 4, 0);
102
-
103
- let out_slice = &mut out[4..];
104
-
105
- if should_parallelize(count, data.len().saturating_sub(4)) {
106
- use rayon::prelude::*;
107
-
108
- // Pre-compute offsets so we can iterate in parallel without allocations
109
- let offsets: Vec<(usize, usize)> = {
110
- let mut offsets = Vec::with_capacity(count);
111
- let mut offset = 4usize;
112
- for _ in 0..count {
113
- if offset + 4 > data.len() {
114
- return Err(Error::from_reason("packed buffer: truncated length"));
115
- }
116
- let len = u32::from_le_bytes([
117
- data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
118
- ]) as usize;
119
- offset += 4;
120
- let end = offset + len;
121
- if end > data.len() {
122
- return Err(Error::from_reason("packed buffer: truncated item"));
123
- }
124
- offsets.push((offset, len));
125
- offset = end;
126
- }
127
- offsets
128
- };
129
-
130
- const CHUNK_ITEMS: usize = 256;
131
-
132
- out_slice
133
- .par_chunks_mut(CHUNK_ITEMS * 4)
134
- .enumerate()
135
- .for_each(|(ci, chunk)| {
136
- let num_items = chunk.len() / 4;
137
- let start = ci * CHUNK_ITEMS;
138
- for (i, item) in offsets[start..start + num_items].iter().enumerate() {
139
- let crc = crc32fast::hash(&data[item.0..item.0 + item.1]);
140
- chunk[i * 4..(i + 1) * 4].copy_from_slice(&crc.to_le_bytes());
141
- }
142
- });
143
- } else {
144
- let mut offset = 4usize;
145
- for i in 0..count {
146
- if offset + 4 > data.len() {
147
- return Err(Error::from_reason("packed buffer: truncated length"));
148
- }
149
- let len = u32::from_le_bytes([
150
- data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
151
- ]) as usize;
152
- offset += 4;
153
- let end = offset + len;
154
- if end > data.len() {
155
- return Err(Error::from_reason("packed buffer: truncated item"));
156
- }
157
- let crc = crc32fast::hash(&data[offset..end]);
158
- out_slice[i * 4..(i + 1) * 4].copy_from_slice(&crc.to_le_bytes());
159
- offset = end;
160
- }
161
- }
162
-
163
- Ok(Buffer::from(out))
164
- }
package/rust/headers.rs DELETED
@@ -1,234 +0,0 @@
1
- // rust/headers.rs — HeaderRefs: zero-alloc packed header parser
2
- // Extracted from ingress.rs for composability
3
- // Uses u64 word-at-a-time comparison for header name matching
4
-
5
- use napi::{Error, Result};
6
-
7
- // ── Header presence flags ─────────────────────────────────────────
8
- const HAS_ORIGIN: u8 = 1 << 0;
9
- const HAS_COOKIE: u8 = 1 << 1;
10
- const HAS_XFF: u8 = 1 << 2;
11
- const HAS_X_REAL_IP: u8 = 1 << 3;
12
- const HAS_XFP: u8 = 1 << 4;
13
- const HAS_ACRM: u8 = 1 << 5;
14
- const HAS_ACRH: u8 = 1 << 6;
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
- /// Load a u64 from the first N bytes of a byte slice, zero-padded.
30
- #[inline(always)]
31
- fn load_u64_padded(bytes: &[u8]) -> u64 {
32
- let mut buf = [0u8; 8];
33
- let len = bytes.len().min(8);
34
- buf[..len].copy_from_slice(&bytes[..len]);
35
- u64::from_le_bytes(buf)
36
- }
37
-
38
- /// Compare two byte slices using u64 word comparison for case-insensitive matching.
39
- /// Both inputs should be lowercase already (or we compute lowercase u64).
40
- #[inline(always)]
41
- fn byte_slice_eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
42
- if a.len() != b.len() {
43
- return false;
44
- }
45
- // For short names (up to 8 bytes) use single u64 comparison
46
- if a.len() <= 8 {
47
- let wa = load_u64_padded(a) | 0x2020_2020_2020_2020;
48
- let wb = load_u64_padded(b) | 0x2020_2020_2020_2020;
49
- return wa == wb;
50
- }
51
- // For longer names: compare first 8 bytes, then remaining
52
- if a.len() <= 16 {
53
- let wa = load_u64_padded(a) | 0x2020_2020_2020_2020;
54
- let wb = load_u64_padded(b) | 0x2020_2020_2020_2020;
55
- if wa != wb {
56
- return false;
57
- }
58
- let wa2 = load_u64_padded(&a[8..]) | 0x2020_2020_2020_2020;
59
- let wb2 = load_u64_padded(&b[8..]) | 0x2020_2020_2020_2020;
60
- return wa2 == wb2;
61
- }
62
- // Fallback
63
- a.eq_ignore_ascii_case(b)
64
- }
65
-
66
- impl<'a> HeaderRefs<'a> {
67
- #[inline(always)]
68
- pub fn empty() -> Self {
69
- Self {
70
- origin: None,
71
- cookie: None,
72
- xff: None,
73
- x_real_ip: None,
74
- x_forwarded_proto: None,
75
- acrm: None,
76
- acrh: None,
77
- flags: 0,
78
- }
79
- }
80
-
81
- /// Parse packed headers from the input buffer.
82
- /// Format: [u16 count] repeated { [u16 name_len] [name] [u32 value_len] [value] }
83
- #[inline]
84
- pub fn parse(packed: &'a [u8], is_options: bool, max_headers: usize) -> Result<Self> {
85
- let mut h = Self::empty();
86
-
87
- if packed.len() < 2 {
88
- return Ok(h);
89
- }
90
-
91
- let count = u16::from_le_bytes([packed[0], packed[1]]) as usize;
92
- if count > max_headers {
93
- return Err(Error::from_reason("too many headers"));
94
- }
95
-
96
- let mut pos = 2usize;
97
-
98
- for _ in 0..count {
99
- if pos + 2 > packed.len() {
100
- return Err(Error::from_reason("malformed packed headers"));
101
- }
102
-
103
- let name_len = u16::from_le_bytes([packed[pos], packed[pos + 1]]) as usize;
104
- pos += 2;
105
-
106
- if pos + name_len > packed.len() {
107
- return Err(Error::from_reason("malformed packed header name"));
108
- }
109
-
110
- let name = &packed[pos..pos + name_len];
111
- pos += name_len;
112
-
113
- if pos + 4 > packed.len() {
114
- return Err(Error::from_reason("malformed packed header value length"));
115
- }
116
-
117
- let value_len = u32::from_le_bytes([
118
- packed[pos],
119
- packed[pos + 1],
120
- packed[pos + 2],
121
- packed[pos + 3],
122
- ]) as usize;
123
- pos += 4;
124
-
125
- if pos + value_len > packed.len() {
126
- return Err(Error::from_reason("malformed packed header value"));
127
- }
128
-
129
- let value = &packed[pos..pos + value_len];
130
- pos += value_len;
131
-
132
- if name.is_empty() {
133
- continue;
134
- }
135
-
136
- let first = name[0] | 0x20;
137
-
138
- // Fast path: dispatch based on first character + name length
139
- if first == b'o' && name.len() == 6 {
140
- if byte_slice_eq_ignore_case(name, b"origin") {
141
- h.flags |= HAS_ORIGIN;
142
- h.origin = Some(value);
143
- }
144
- } else if first == b'c' && name.len() == 6 {
145
- if byte_slice_eq_ignore_case(name, b"cookie") {
146
- h.flags |= HAS_COOKIE;
147
- h.cookie = Some(value);
148
- }
149
- } else if first == b'x' {
150
- let name_len = name.len();
151
- if name_len == 15 && byte_slice_eq_ignore_case(name, b"x-forwarded-for") {
152
- h.flags |= HAS_XFF;
153
- h.xff = Some(value);
154
- } else if name_len == 9 && byte_slice_eq_ignore_case(name, b"x-real-ip") {
155
- h.flags |= HAS_X_REAL_IP;
156
- h.x_real_ip = Some(value);
157
- } else if name_len == 17 && byte_slice_eq_ignore_case(name, b"x-forwarded-proto") {
158
- h.flags |= HAS_XFP;
159
- h.x_forwarded_proto = Some(value);
160
- }
161
- } else if first == b'a' && is_options {
162
- if name.len() == 29 && byte_slice_eq_ignore_case(name, b"access-control-request-method") {
163
- h.flags |= HAS_ACRM;
164
- h.acrm = Some(value);
165
- } else if name.len() == 30 && byte_slice_eq_ignore_case(name, b"access-control-request-headers") {
166
- h.flags |= HAS_ACRH;
167
- h.acrh = Some(value);
168
- }
169
- }
170
- }
171
-
172
- Ok(h)
173
- }
174
-
175
- #[inline(always)]
176
- pub fn origin(&self) -> Option<&[u8]> {
177
- self.origin
178
- }
179
-
180
- #[inline(always)]
181
- pub fn cookie(&self) -> Option<&[u8]> {
182
- self.cookie
183
- }
184
-
185
- #[inline(always)]
186
- pub fn xff(&self) -> Option<&[u8]> {
187
- self.xff
188
- }
189
-
190
- #[inline(always)]
191
- pub fn x_real_ip(&self) -> Option<&[u8]> {
192
- self.x_real_ip
193
- }
194
-
195
- #[inline(always)]
196
- pub fn x_forwarded_proto(&self) -> Option<&[u8]> {
197
- self.x_forwarded_proto
198
- }
199
-
200
- #[inline(always)]
201
- pub fn acrm(&self) -> Option<&[u8]> {
202
- self.acrm
203
- }
204
-
205
- #[inline(always)]
206
- pub fn acrh(&self) -> Option<&[u8]> {
207
- self.acrh
208
- }
209
-
210
- #[inline(always)]
211
- pub fn has_origin(&self) -> bool {
212
- (self.flags & HAS_ORIGIN) != 0
213
- }
214
-
215
- #[inline(always)]
216
- pub fn has_cookie(&self) -> bool {
217
- (self.flags & HAS_COOKIE) != 0
218
- }
219
-
220
- #[inline(always)]
221
- pub fn has_xfp(&self) -> bool {
222
- (self.flags & HAS_XFP) != 0
223
- }
224
-
225
- #[inline(always)]
226
- pub fn has_acrm(&self) -> bool {
227
- (self.flags & HAS_ACRM) != 0
228
- }
229
-
230
- #[inline(always)]
231
- pub fn has_acrh(&self) -> bool {
232
- (self.flags & HAS_ACRH) != 0
233
- }
234
- }