castrum 0.9.2 → 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.
Files changed (184) hide show
  1. package/Cargo.lock +2259 -0
  2. package/Cargo.toml +138 -0
  3. package/README.md +6 -0
  4. package/build.rs +5 -0
  5. package/castrum.linux-x64-gnu.node +0 -0
  6. package/dist/index.d.ts +1348 -1295
  7. package/dist/index.js +4307 -3776
  8. package/docs/API.md +1 -1
  9. package/docs/ARCHITECTURE.md +3 -2
  10. package/docs/CASE_STUDY.md +2 -2
  11. package/docs/ENVIRONMENT.md +16 -0
  12. package/docs/FFI_BUN_GUIDE.md +89 -6
  13. package/docs/REPO_MAP.md +2 -2
  14. package/index.ts +19 -25
  15. package/package.json +9 -1
  16. package/rust/crypto/aead.rs +378 -0
  17. package/rust/crypto/argon2.rs +265 -0
  18. package/rust/crypto/base64.rs +420 -0
  19. package/rust/crypto/bcrypt.rs +89 -0
  20. package/rust/crypto/cookie_sign.rs +169 -0
  21. package/rust/crypto/csrf.rs +106 -0
  22. package/rust/crypto/ed25519.rs +215 -0
  23. package/rust/crypto/hashing.rs +140 -0
  24. package/rust/crypto/hmac_sha256.rs +88 -0
  25. package/rust/crypto/jwt/api.rs +286 -0
  26. package/rust/crypto/jwt/mod.rs +22 -0
  27. package/rust/crypto/jwt/tests.rs +312 -0
  28. package/rust/crypto/jwt/token.rs +360 -0
  29. package/rust/crypto/mod.rs +27 -0
  30. package/rust/crypto/pbkdf2.rs +96 -0
  31. package/rust/crypto/random_token.rs +85 -0
  32. package/rust/crypto/session.rs +222 -0
  33. package/rust/ffi/crypto.rs +708 -0
  34. package/rust/ffi/hashing.rs +285 -0
  35. package/rust/ffi/http.rs +752 -0
  36. package/rust/ffi/ingress.rs +303 -0
  37. package/rust/ffi/json.rs +517 -0
  38. package/rust/ffi/jwt.rs +419 -0
  39. package/rust/ffi/metrics.rs +373 -0
  40. package/rust/ffi/mod.rs +68 -0
  41. package/rust/ffi/payload.rs +275 -0
  42. package/rust/ffi/probe.rs +60 -0
  43. package/rust/ffi/rate_limit.rs +77 -0
  44. package/rust/ffi/route.rs +85 -0
  45. package/rust/ffi/tests.rs +2333 -0
  46. package/rust/ffi/util.rs +152 -0
  47. package/rust/ffi/validators.rs +123 -0
  48. package/rust/http/accept.rs +606 -0
  49. package/rust/http/cookie_parser.rs +140 -0
  50. package/rust/http/etag.rs +289 -0
  51. package/rust/http/form.rs +132 -0
  52. package/rust/http/headers.rs +267 -0
  53. package/rust/http/http_date.rs +286 -0
  54. package/rust/http/http_parser.rs +252 -0
  55. package/rust/http/media_type.rs +355 -0
  56. package/rust/http/method.rs +200 -0
  57. package/rust/http/mime_lookup.rs +130 -0
  58. package/rust/http/mod.rs +32 -0
  59. package/rust/http/multipart.rs +685 -0
  60. package/rust/http/query_parser.rs +180 -0
  61. package/rust/http/url_codec.rs +296 -0
  62. package/rust/http/url_join.rs +324 -0
  63. package/rust/ingress/api.rs +355 -0
  64. package/rust/ingress/cors.rs +403 -0
  65. package/rust/ingress/ingress_constants.rs +209 -0
  66. package/rust/ingress/ip_trust.rs +312 -0
  67. package/rust/ingress/mod.rs +48 -0
  68. package/rust/ingress/native_route.rs +779 -0
  69. package/rust/ingress/options.rs +176 -0
  70. package/rust/ingress/output.rs +406 -0
  71. package/rust/ingress/packed.rs +191 -0
  72. package/rust/ingress/pipeline.rs +534 -0
  73. package/rust/ingress/proxy.rs +138 -0
  74. package/rust/ingress/rate_limit.rs +538 -0
  75. package/rust/ingress/terminal.rs +114 -0
  76. package/rust/ingress/tests.rs +566 -0
  77. package/rust/ingress/time.rs +70 -0
  78. package/rust/json/fast_schema/capture.rs +336 -0
  79. package/rust/json/fast_schema/compile.rs +556 -0
  80. package/rust/json/fast_schema/cursor.rs +368 -0
  81. package/rust/json/fast_schema/email.rs +282 -0
  82. package/rust/json/fast_schema/errors.rs +247 -0
  83. package/rust/json/fast_schema/mod.rs +57 -0
  84. package/rust/json/fast_schema/tests.rs +1143 -0
  85. package/rust/json/fast_schema/types.rs +140 -0
  86. package/rust/json/fast_schema/validate.rs +931 -0
  87. package/rust/json/json_ops.rs +121 -0
  88. package/rust/json/json_schema.rs +837 -0
  89. package/rust/json/json_ser.rs +881 -0
  90. package/rust/json/mod.rs +19 -0
  91. package/rust/json/napi_marshal.rs +318 -0
  92. package/rust/json/patch/api.rs +60 -0
  93. package/rust/json/patch/engine.rs +153 -0
  94. package/rust/json/patch/mod.rs +47 -0
  95. package/rust/json/patch/ops.rs +270 -0
  96. package/rust/json/patch/pointer.rs +49 -0
  97. package/rust/json/patch/tests.rs +499 -0
  98. package/rust/lib.rs +72 -0
  99. package/rust/metrics/api.rs +148 -0
  100. package/rust/metrics/mod.rs +21 -0
  101. package/rust/metrics/registry.rs +961 -0
  102. package/rust/panic_safety.rs +40 -0
  103. package/rust/payload/compress.rs +512 -0
  104. package/rust/payload/mod.rs +13 -0
  105. package/rust/payload/sse.rs +214 -0
  106. package/rust/payload/template.rs +253 -0
  107. package/rust/payload/websocket.rs +69 -0
  108. package/rust/payload/ws_frames.rs +354 -0
  109. package/rust/proptest_suite.rs +139 -0
  110. package/rust/selection.rs +95 -0
  111. package/rust/src/bin/native-bench.rs +495 -0
  112. package/rust/test_support.rs +71 -0
  113. package/rust/util/batch/api.rs +266 -0
  114. package/rust/util/batch/core.rs +473 -0
  115. package/rust/util/batch/mod.rs +23 -0
  116. package/rust/util/batch/tests.rs +445 -0
  117. package/rust/util/batch_core.rs +184 -0
  118. package/rust/util/bytes.rs +407 -0
  119. package/rust/util/mod.rs +34 -0
  120. package/rust/util/packed.rs +473 -0
  121. package/rust/util/text.rs +130 -0
  122. package/rust/util/threadpool.rs +128 -0
  123. package/rust/util/validation.rs +419 -0
  124. package/rust-toolchain.toml +17 -0
  125. package/scripts/postinstall.mjs +199 -0
  126. package/src/ingress/constants.ts +1 -1
  127. package/src/ingress/context.ts +2 -2
  128. package/src/ingress/decode/baked-result.ts +13 -13
  129. package/src/ingress/decode/fast-result.ts +17 -17
  130. package/src/ingress/errors.ts +7 -7
  131. package/src/ingress/headers/baked-templates.ts +1 -1
  132. package/src/ingress/headers/fast-templates.ts +3 -3
  133. package/src/ingress/index.ts +1 -1
  134. package/src/ingress/native-route.ts +9 -5
  135. package/src/ingress/options.ts +1 -1
  136. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  137. package/src/ingress/packing/route-wire.ts +1 -5
  138. package/src/ingress/response/error-bodies.ts +5 -5
  139. package/src/ingress/response/terminal.ts +1 -1
  140. package/src/ingress/routes/echo.ts +4 -4
  141. package/src/ingress/routes/fallback.ts +2 -2
  142. package/src/ingress/routes/head.ts +2 -2
  143. package/src/ingress/routes/native.ts +2 -1
  144. package/src/ingress/routes/options.ts +1 -1
  145. package/src/ingress/routes/read.ts +2 -2
  146. package/src/ingress/server.ts +10 -6
  147. package/src/ingress/status.ts +7 -7
  148. package/src/integration/batch.ts +1 -1
  149. package/src/integration/index.ts +9 -9
  150. package/src/integration/pipeline.ts +6 -6
  151. package/src/integration/websocket.ts +2 -2
  152. package/src/loader/index.ts +1 -5
  153. package/src/loader/ops.ts +1 -1
  154. package/src/native/ffi/build/codecs.ts +104 -8
  155. package/src/native/ffi/build/compress.ts +14 -10
  156. package/src/native/ffi/build/instances.ts +40 -3
  157. package/src/native/ffi/build/metrics.ts +224 -0
  158. package/src/native/ffi/build/parse.ts +59 -23
  159. package/src/native/ffi/build/util.ts +14 -1
  160. package/src/native/ffi/build.ts +2 -0
  161. package/src/native/ffi/selftest.ts +10 -3
  162. package/src/native/ffi/types.ts +111 -9
  163. package/src/native/ffi.ts +87 -1
  164. package/src/native/index.ts +1 -1
  165. package/src/native/loader.ts +2 -2
  166. package/src/native/types/instances.ts +34 -0
  167. package/src/native/types.ts +11 -0
  168. package/src/runtime/codec.ts +53 -9
  169. package/src/runtime/types.ts +7 -0
  170. package/src/rust-ffi/batch/build.ts +16 -9
  171. package/src/rust-ffi/index.ts +7 -6
  172. package/src/rust-ffi/options.ts +1 -3
  173. package/src/rust-ffi/proven.ts +3 -3
  174. package/src/rust-ffi/scalar/crypto.ts +1 -1
  175. package/src/rust-ffi/scalar/factories.ts +73 -0
  176. package/src/rust-ffi/scalar/hashing.ts +1 -1
  177. package/src/rust-ffi/scalar/http.ts +17 -11
  178. package/src/rust-ffi/scalar/index.ts +2 -0
  179. package/src/rust-ffi/scalar/interface.ts +20 -0
  180. package/src/rust-ffi/scalar/payload.ts +11 -3
  181. package/src/rust-ffi/scalar/validation.ts +121 -0
  182. package/src/selection.ts +1 -1
  183. package/src/shared/bytes.ts +5 -4
  184. package/src/shared/codec.ts +18 -3
@@ -0,0 +1,407 @@
1
+ // rust/util/bytes.rs — Shared low-level byte utilities
2
+ //
3
+ // This is the ONE home for the small byte-manipulation primitives that are
4
+ // reused across the crate: word-at-a-time comparison, hex (de)encoding,
5
+ // `%XX` percent decoding, whitespace trimming, and cookie pair splitting.
6
+ //
7
+ // Before the refactor these helpers were copy-pasted into several modules
8
+ // (headers.rs, method.rs, hmac_sha256.rs, random_token.rs, url_codec.rs,
9
+ // query_parser.rs, cookie_parser.rs, json_ser.rs). Keeping them in a single
10
+ // place means there is exactly ONE implementation to read, test, and tune.
11
+
12
+ // ── Word-at-a-time comparison ──────────────────────────────────────
13
+
14
+ /// Load a `u64` from the first `min(len, 8)` bytes of `bytes`, zero-padded.
15
+ #[inline(always)]
16
+ pub fn load_u64_padded(bytes: &[u8]) -> u64 {
17
+ let mut buf = [0u8; 8];
18
+ let len = bytes.len().min(8);
19
+ buf[..len].copy_from_slice(&bytes[..len]);
20
+ u64::from_le_bytes(buf)
21
+ }
22
+
23
+ /// ASCII case-insensitive comparison using u64 words for short inputs.
24
+ ///
25
+ /// Both inputs must have the same length. The `0x20` bit is OR'd into both
26
+ /// words so `A`..`Z` and `a`..`z` collapse to the same value — no allocation.
27
+ ///
28
+ /// Note: only safe for ASCII name matching (headers, HTTP methods), never for
29
+ /// arbitrary binary data (the `0x20` mask can equate some non-alpha chars).
30
+ #[inline(always)]
31
+ pub fn ascii_eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
32
+ if a.len() != b.len() {
33
+ return false;
34
+ }
35
+
36
+ match a.len() {
37
+ 0 => true,
38
+ 1..=8 => {
39
+ let wa = load_u64_padded(a) | 0x2020_2020_2020_2020;
40
+ let wb = load_u64_padded(b) | 0x2020_2020_2020_2020;
41
+ wa == wb
42
+ }
43
+ 9..=16 => {
44
+ let wa = load_u64_padded(a) | 0x2020_2020_2020_2020;
45
+ let wb = load_u64_padded(b) | 0x2020_2020_2020_2020;
46
+ if wa != wb {
47
+ return false;
48
+ }
49
+ let wa2 = load_u64_padded(&a[8..]) | 0x2020_2020_2020_2020;
50
+ let wb2 = load_u64_padded(&b[8..]) | 0x2020_2020_2020_2020;
51
+ wa2 == wb2
52
+ }
53
+ _ => a.eq_ignore_ascii_case(b),
54
+ }
55
+ }
56
+
57
+ // ── Hex (de)encoding ───────────────────────────────────────────────
58
+
59
+ pub const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
60
+ pub const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
61
+
62
+ /// 256-entry hex-digit lookup (single table load instead of a range-compare
63
+ /// chain); -1 marks a non-hex byte. Shared by all `%XX` / hex decode paths.
64
+ const HEX_VAL_LUT: [i8; 256] = {
65
+ let mut t = [-1i8; 256];
66
+ let mut i = 0usize;
67
+ while i < 256 {
68
+ let b = i as u8;
69
+ t[i] = match b {
70
+ b'0'..=b'9' => (b - b'0') as i8,
71
+ b'a'..=b'f' => (b - b'a' + 10) as i8,
72
+ b'A'..=b'F' => (b - b'A' + 10) as i8,
73
+ _ => -1,
74
+ };
75
+ i += 1;
76
+ }
77
+ t
78
+ };
79
+
80
+ /// Value of a single hex digit, or `None` if `b` is not a hex digit.
81
+ #[inline(always)]
82
+ pub fn hex_val(b: u8) -> Option<u8> {
83
+ let v = HEX_VAL_LUT[b as usize];
84
+ if v < 0 {
85
+ None
86
+ } else {
87
+ Some(v as u8)
88
+ }
89
+ }
90
+
91
+ /// Encode `bytes` as lowercase hex into `out`, which must hold `2 * bytes.len()`.
92
+ #[inline]
93
+ pub fn hex_encode(bytes: &[u8], out: &mut [u8]) {
94
+ // Checked (not debug-only): with `debug-assertions = false` an undersized
95
+ // `out` would otherwise surface as an unhelpful index panic in release.
96
+ assert!(
97
+ out.len() >= bytes.len() * 2,
98
+ "hex_encode: output buffer too small ({} < {})",
99
+ out.len(),
100
+ bytes.len() * 2
101
+ );
102
+ for (i, &b) in bytes.iter().enumerate() {
103
+ out[2 * i] = HEX_LOWER[(b >> 4) as usize];
104
+ out[2 * i + 1] = HEX_LOWER[(b & 0x0f) as usize];
105
+ }
106
+ }
107
+
108
+ /// Encode exactly 32 bytes as lowercase hex into a fixed 64-byte output.
109
+ #[inline(always)]
110
+ pub fn hex_encode_32(bytes: &[u8], out: &mut [u8; 64]) {
111
+ debug_assert_eq!(bytes.len(), 32);
112
+ hex_encode(bytes, out);
113
+ }
114
+
115
+ /// Decode exactly 64 hex characters into 32 bytes. Returns `None` on bad input.
116
+ #[inline]
117
+ pub fn hex_decode_32(hex: &[u8]) -> Option<[u8; 32]> {
118
+ if hex.len() != 64 {
119
+ return None;
120
+ }
121
+ let mut out = [0u8; 32];
122
+ for (i, slot) in out.iter_mut().enumerate() {
123
+ let hi = hex_val(hex[2 * i])?;
124
+ let lo = hex_val(hex[2 * i + 1])?;
125
+ *slot = (hi << 4) | lo;
126
+ }
127
+ Some(out)
128
+ }
129
+
130
+ // ── Percent decoding ───────────────────────────────────────────────
131
+
132
+ /// Decode the `%XX` sequence starting at `src[i..]` into a single byte.
133
+ ///
134
+ /// Returns `(decoded_byte, index_of_next_input_byte)`, or `None` when the
135
+ /// sequence is malformed (truncated or a non-hex digit).
136
+ #[inline]
137
+ pub fn decode_percent_at(src: &[u8], i: usize) -> Option<(u8, usize)> {
138
+ if i + 2 >= src.len() {
139
+ return None;
140
+ }
141
+ let hi = hex_val(src[i + 1])?;
142
+ let lo = hex_val(src[i + 2])?;
143
+ Some(((hi << 4) | lo, i + 3))
144
+ }
145
+
146
+ // ── Form-component decoding ────────────────────────────────────────
147
+
148
+ /// Decode failure modes for [`decode_form_component_into`].
149
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
150
+ pub enum FormDecodeError {
151
+ /// Malformed `%XX` sequence (truncated or a non-hex digit).
152
+ Malformed,
153
+ /// `out` cannot hold the decoded result.
154
+ BufferTooSmall,
155
+ }
156
+
157
+ /// URL-decode a form component (`+` → space, `%XX` → byte) into `out`,
158
+ /// returning the number of bytes written.
159
+ ///
160
+ /// The decoded length never exceeds `src.len()` (each input byte maps to at
161
+ /// most one output byte). Callers that pre-size `out` to `src.len()` can never
162
+ /// observe [`FormDecodeError::BufferTooSmall`]; callers that pass the remaining
163
+ /// tail of a larger buffer get a precise capacity check against the ACTUAL
164
+ /// decoded length (a `%XX` sequence shrinks 3 input bytes to 1, so a buffer
165
+ /// sized to the decoded form succeeds).
166
+ ///
167
+ /// Single shared implementation for `query_parser::write_decoded_form_component`
168
+ /// (length-prefixed slice writer) and `json_ser::decode_query_component`
169
+ /// (Vec appender) — the wire decode loop lives in exactly one place.
170
+ #[inline]
171
+ pub fn decode_form_component_into(
172
+ src: &[u8],
173
+ out: &mut [u8],
174
+ ) -> std::result::Result<usize, FormDecodeError> {
175
+ if memchr::memchr2(b'+', b'%', src).is_none() {
176
+ if out.len() < src.len() {
177
+ return Err(FormDecodeError::BufferTooSmall);
178
+ }
179
+ out[..src.len()].copy_from_slice(src);
180
+ return Ok(src.len());
181
+ }
182
+ // Run-copy decode: bulk-copy plain runs up to the next '+'/'%', then
183
+ // decode the single special byte, instead of walking every byte. This is
184
+ // the same memchr2 discipline `url_decode` uses and avoids per-byte
185
+ // branches over the common long runs of unreserved characters. Capacity
186
+ // is still checked against the ACTUAL decoded length (a `%XX` shrinks 3
187
+ // input bytes to 1), preserving the existing semantics.
188
+ let mut i = 0usize;
189
+ let mut written = 0usize;
190
+ while i < src.len() {
191
+ match memchr::memchr2(b'+', b'%', &src[i..]) {
192
+ Some(rel) => {
193
+ let run_end = i + rel;
194
+ let run = &src[i..run_end];
195
+ if written + run.len() > out.len() {
196
+ return Err(FormDecodeError::BufferTooSmall);
197
+ }
198
+ out[written..written + run.len()].copy_from_slice(run);
199
+ written += run.len();
200
+ i = run_end;
201
+
202
+ // Decode the '+' or '%XX' at position i.
203
+ let (b, next) = match src[i] {
204
+ b'+' => (b' ', i + 1),
205
+ _ => {
206
+ let (byte, next) =
207
+ decode_percent_at(src, i).ok_or(FormDecodeError::Malformed)?;
208
+ (byte, next)
209
+ }
210
+ };
211
+ if written >= out.len() {
212
+ return Err(FormDecodeError::BufferTooSmall);
213
+ }
214
+ out[written] = b;
215
+ written += 1;
216
+ i = next;
217
+ }
218
+ None => {
219
+ let run = &src[i..];
220
+ if written + run.len() > out.len() {
221
+ return Err(FormDecodeError::BufferTooSmall);
222
+ }
223
+ out[written..written + run.len()].copy_from_slice(run);
224
+ written += run.len();
225
+ i = src.len();
226
+ }
227
+ }
228
+ }
229
+ Ok(written)
230
+ }
231
+
232
+ /// Compute the decoded length of `src` WITHOUT writing (mirror of
233
+ /// [`decode_form_component_into`], minus the output). Used by the C-ABI
234
+ /// "needed-size" convention: when a packed/JSON writer hits a too-small output
235
+ /// buffer, the caller runs this exact-size pass ONCE (only on the rare miss)
236
+ /// and reports the required size so the JS wrapper can `growExact` and retry —
237
+ /// no doubling re-run loop, no 9×/8× pre-size.
238
+ ///
239
+ /// Same `Malformed` signal on a bad `%XX`, so a caller can disambiguate a
240
+ /// too-small buffer from a real parse error.
241
+ #[inline]
242
+ pub fn decode_form_component_len(src: &[u8]) -> std::result::Result<usize, FormDecodeError> {
243
+ if memchr::memchr2(b'+', b'%', src).is_none() {
244
+ return Ok(src.len());
245
+ }
246
+ let mut i = 0usize;
247
+ let mut written = 0usize;
248
+ while i < src.len() {
249
+ match memchr::memchr2(b'+', b'%', &src[i..]) {
250
+ Some(rel) => {
251
+ written += rel;
252
+ i += rel;
253
+ let next = match src[i] {
254
+ b'+' => i + 1,
255
+ _ => decode_percent_at(src, i)
256
+ .map(|(_, next)| next)
257
+ .ok_or(FormDecodeError::Malformed)?,
258
+ };
259
+ written += 1;
260
+ i = next;
261
+ }
262
+ None => {
263
+ written += src.len() - i;
264
+ i = src.len();
265
+ }
266
+ }
267
+ }
268
+ Ok(written)
269
+ }
270
+
271
+ // ── Whitespace + cookie splitting ──────────────────────────────────
272
+
273
+ /// Trim ASCII whitespace from both ends of a byte slice.
274
+ #[inline(always)]
275
+ pub fn trim_ascii_whitespace(bytes: &[u8]) -> &[u8] {
276
+ let mut start = 0usize;
277
+ let mut end = bytes.len();
278
+
279
+ while start < end && bytes[start].is_ascii_whitespace() {
280
+ start += 1;
281
+ }
282
+
283
+ while end > start && bytes[end - 1].is_ascii_whitespace() {
284
+ end -= 1;
285
+ }
286
+
287
+ &bytes[start..end]
288
+ }
289
+
290
+ /// Iterate over the `name=value` pairs in a cookie-style header.
291
+ ///
292
+ /// Pairs are split on `;`, whitespace-trimmed, and entries with an empty name
293
+ /// are skipped. A missing `=` yields an empty value. A value wrapped in
294
+ /// surrounding double quotes is unwrapped (RFC 6265 §5.2) so the native
295
+ /// output is byte-identical to the JS fallback.
296
+ pub fn cookie_pairs(input: &[u8]) -> impl Iterator<Item = (&[u8], &[u8])> + '_ {
297
+ input.split(|&b| b == b';').filter_map(|raw| {
298
+ let pair = trim_ascii_whitespace(raw);
299
+ if pair.is_empty() {
300
+ return None;
301
+ }
302
+
303
+ let (name, value) = match pair.iter().position(|&b| b == b'=') {
304
+ Some(eq) => (&pair[..eq], &pair[eq + 1..]),
305
+ None => (pair, &[][..]),
306
+ };
307
+
308
+ let name = trim_ascii_whitespace(name);
309
+ let value = trim_ascii_whitespace(value);
310
+
311
+ if name.is_empty() {
312
+ return None;
313
+ }
314
+
315
+ // RFC 6265 §5.2: unwrap a cookie-value surrounded by DQUOTE (only when
316
+ // BOTH ends quote, matching the JS fallback's `unquote` exactly).
317
+ let value = if value.len() >= 2 && value[0] == b'"' && value[value.len() - 1] == b'"' {
318
+ &value[1..value.len() - 1]
319
+ } else {
320
+ value
321
+ };
322
+
323
+ Some((name, value))
324
+ })
325
+ }
326
+
327
+ #[cfg(test)]
328
+ mod tests {
329
+ use super::*;
330
+
331
+ fn decode(src: &[u8]) -> Vec<u8> {
332
+ let mut out = vec![0u8; src.len()];
333
+ let n = decode_form_component_into(src, &mut out).unwrap();
334
+ out.truncate(n);
335
+ out
336
+ }
337
+
338
+ #[test]
339
+ fn decode_form_plain_passthrough() {
340
+ assert_eq!(decode(b"abc"), b"abc");
341
+ assert_eq!(decode(b""), b"");
342
+ // The fast path copies exactly; verify capacity via the full slice.
343
+ let mut out = [0u8; 3];
344
+ assert_eq!(decode_form_component_into(b"abc", &mut out), Ok(3));
345
+ }
346
+
347
+ #[test]
348
+ fn decode_form_plus_and_percent() {
349
+ assert_eq!(decode(b"a+b"), b"a b");
350
+ assert_eq!(decode(b"%41%42"), b"AB");
351
+ assert_eq!(decode(b"q=%E2%9C%93"), b"q=\xE2\x9C\x93");
352
+ assert_eq!(decode(b"%2B"), b"+");
353
+ }
354
+
355
+ #[test]
356
+ fn decode_form_malformed_percent() {
357
+ let mut out = [0u8; 8];
358
+ assert_eq!(
359
+ decode_form_component_into(b"%ZZ", &mut out),
360
+ Err(FormDecodeError::Malformed)
361
+ );
362
+ assert_eq!(
363
+ decode_form_component_into(b"%4", &mut out),
364
+ Err(FormDecodeError::Malformed)
365
+ );
366
+ assert_eq!(
367
+ decode_form_component_into(b"x%", &mut out),
368
+ Err(FormDecodeError::Malformed)
369
+ );
370
+ }
371
+
372
+ #[test]
373
+ fn cookie_pairs_unwraps_dquote() {
374
+ let pairs: Vec<(&[u8], &[u8])> =
375
+ cookie_pairs(b"a=1; b=\"quoted value\"; c=\"\"; d=\"abc; empty=").collect();
376
+ assert_eq!(
377
+ pairs,
378
+ vec![
379
+ (&b"a"[..], &b"1"[..]),
380
+ (&b"b"[..], &b"quoted value"[..]),
381
+ (&b"c"[..], &b""[..]),
382
+ (&b"d"[..], &b"\"abc"[..]), // unbalanced quote kept, matches JS
383
+ (&b"empty"[..], &b""[..]),
384
+ ]
385
+ );
386
+ }
387
+
388
+ #[test]
389
+ fn cookie_pairs_keeps_unbalanced_quotes() {
390
+ // A quote only at one end (or embedded) is left as-is — matches JS.
391
+ let pairs: Vec<(&[u8], &[u8])> = cookie_pairs(b"a=\"abc; b=ab\"c; c=\"quote").collect();
392
+ assert_eq!(
393
+ pairs,
394
+ vec![
395
+ (&b"a"[..], &b"\"abc"[..]),
396
+ (&b"b"[..], &b"ab\"c"[..]),
397
+ (&b"c"[..], &b"\"quote"[..]),
398
+ ]
399
+ );
400
+ }
401
+ #[test]
402
+ #[should_panic(expected = "hex_encode: output buffer too small")]
403
+ fn hex_encode_panics_on_undersized() {
404
+ let mut out = [0u8; 4];
405
+ super::hex_encode(b"abcd", &mut out);
406
+ }
407
+ }
@@ -0,0 +1,34 @@
1
+ //! Shared infrastructure (no napi types in core signatures).
2
+ //
3
+ // Task-focused modules (no napi types in core signatures):
4
+ // - bytes.rs byte primitives (word-compare, hex, %XX decode, trim, cookie_pairs)
5
+ // - packed.rs zero-alloc packed iterators + byte writers
6
+ // - batch/ packed aggregate batch helpers (core.rs routing + api.rs napi)
7
+ // - batch_core.rs generic rayon-parallel batch helpers
8
+ // - threadpool.rs rayon global pool init + parallelism heuristics
9
+ // - validation.rs email / UUID / IPv4 / IPv6 + batch fixed-width hex validators
10
+ // - text.rs text utilities (JS-RegExp metacharacter escaping)
11
+ //
12
+ // The `pub use` re-exports below keep existing `crate::util::*` call sites and
13
+ // the napi JS exports (`initThreadPool` / `rayonNumThreads`) working
14
+ // unchanged. Prefer importing from the specific submodules in new code.
15
+
16
+ pub mod batch;
17
+ pub mod batch_core;
18
+ pub mod bytes;
19
+ pub mod packed;
20
+ pub mod text;
21
+ pub mod threadpool;
22
+ pub mod validation;
23
+
24
+ // Re-exported byte primitives so existing callers keep working; the canonical
25
+ // implementations live in `bytes.rs`.
26
+ pub use self::bytes::trim_ascii_whitespace;
27
+
28
+ pub(crate) use self::batch::{run_bitset_batch, run_packed_batch, run_packed_batch_idx};
29
+ pub use self::batch_core::{count_batch, sum_batch_i64, validation_bitset_chunked};
30
+ pub use self::packed::{
31
+ ensure_capacity, run_packed_into, slices_overlap, total_bytes, unpack, write_bitset_batch_into,
32
+ write_bytes, write_bytes_lowercase, write_sum_batch_into, write_u32_batch_into, write_u32_le,
33
+ };
34
+ pub use self::threadpool::{init_thread_pool, rayon_num_threads, should_parallelize};