castrum 0.7.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 (86) hide show
  1. package/Cargo.toml +73 -0
  2. package/README.md +432 -0
  3. package/bench.ts +3 -0
  4. package/castrum.linux-x64-gnu.node +0 -0
  5. package/index.ts +24 -0
  6. package/package.json +94 -0
  7. package/rust/batch.rs +195 -0
  8. package/rust/cookie_parser.rs +70 -0
  9. package/rust/cors.rs +263 -0
  10. package/rust/hashing.rs +164 -0
  11. package/rust/headers.rs +234 -0
  12. package/rust/hmac_sha256.rs +113 -0
  13. package/rust/http_parser.rs +132 -0
  14. package/rust/ingress.rs +616 -0
  15. package/rust/ingress_constants.rs +89 -0
  16. package/rust/ip_trust.rs +185 -0
  17. package/rust/json_ops.rs +66 -0
  18. package/rust/json_patch_ops.rs +25 -0
  19. package/rust/json_schema.rs +151 -0
  20. package/rust/json_ser.rs +429 -0
  21. package/rust/lib.rs +48 -0
  22. package/rust/method.rs +141 -0
  23. package/rust/mime_lookup.rs +76 -0
  24. package/rust/output.rs +238 -0
  25. package/rust/proxy.rs +90 -0
  26. package/rust/query_parser.rs +141 -0
  27. package/rust/random_token.rs +52 -0
  28. package/rust/rate_limit.rs +203 -0
  29. package/rust/terminal.rs +95 -0
  30. package/rust/unit_tests.rs +843 -0
  31. package/rust/url_codec.rs +133 -0
  32. package/rust/util.rs +537 -0
  33. package/rust/validation.rs +107 -0
  34. package/rust/websocket.rs +23 -0
  35. package/src/baseline/index.ts +12 -0
  36. package/src/baseline/tasks/cookie.ts +22 -0
  37. package/src/baseline/tasks/hashing.ts +18 -0
  38. package/src/baseline/tasks/hmac.ts +38 -0
  39. package/src/baseline/tasks/http.ts +62 -0
  40. package/src/baseline/tasks/json-patch.ts +12 -0
  41. package/src/baseline/tasks/json.ts +50 -0
  42. package/src/baseline/tasks/mime.ts +5 -0
  43. package/src/baseline/tasks/query.ts +14 -0
  44. package/src/baseline/tasks/token.ts +15 -0
  45. package/src/baseline/tasks/url.ts +11 -0
  46. package/src/baseline/tasks/validation.ts +19 -0
  47. package/src/baseline/tasks/websocket.ts +14 -0
  48. package/src/bench/assert.ts +70 -0
  49. package/src/bench/checks.ts +227 -0
  50. package/src/bench/checksum.ts +27 -0
  51. package/src/bench/comparisons.ts +70 -0
  52. package/src/bench/concurrent-worker.ts +135 -0
  53. package/src/bench/fixtures.ts +233 -0
  54. package/src/bench/index.ts +2 -0
  55. package/src/bench/measure.ts +177 -0
  56. package/src/bench/now.ts +3 -0
  57. package/src/bench/report.ts +79 -0
  58. package/src/bench/run.ts +57 -0
  59. package/src/bench/tasks/complex.ts +232 -0
  60. package/src/bench/tasks/concurrent.ts +213 -0
  61. package/src/bench/tasks/cookie.ts +44 -0
  62. package/src/bench/tasks/hashing.ts +33 -0
  63. package/src/bench/tasks/hmac.ts +36 -0
  64. package/src/bench/tasks/http.ts +50 -0
  65. package/src/bench/tasks/index.ts +52 -0
  66. package/src/bench/tasks/json-patch.ts +21 -0
  67. package/src/bench/tasks/json.ts +33 -0
  68. package/src/bench/tasks/mime.ts +21 -0
  69. package/src/bench/tasks/query.ts +44 -0
  70. package/src/bench/tasks/stress.ts +142 -0
  71. package/src/bench/tasks/token.ts +20 -0
  72. package/src/bench/tasks/url.ts +34 -0
  73. package/src/bench/tasks/validation.ts +57 -0
  74. package/src/bench/tasks/websocket.ts +21 -0
  75. package/src/bench/types.ts +60 -0
  76. package/src/data/json-rows.ts +33 -0
  77. package/src/ingress/constants.ts +56 -0
  78. package/src/ingress/fast.ts +1082 -0
  79. package/src/ingress/handlers.ts +1202 -0
  80. package/src/ingress/index.ts +457 -0
  81. package/src/ingress/packed-input.ts +59 -0
  82. package/src/native/index.ts +207 -0
  83. package/src/rust-ffi/index.ts +555 -0
  84. package/src/shared/bytes.ts +36 -0
  85. package/src/shared/json.ts +18 -0
  86. package/src/shared/packed.ts +316 -0
package/rust/output.rs ADDED
@@ -0,0 +1,238 @@
1
+ // rust/output.rs — Ingress output buffer layout and low-level write helpers
2
+ // Extracted from ingress.rs for composability
3
+ // Uses Buf as a lightweight slice cursor for zero-cost output serialization
4
+
5
+ use std::ptr;
6
+
7
+ // ── Output buffer binary layout ───────────────────────────────────
8
+ pub const OUT_VERDICT: usize = 0;
9
+ pub const OUT_ERROR_CODE: usize = 1;
10
+ pub const OUT_STATUS: usize = 2;
11
+ pub const OUT_FLAGS: usize = 4;
12
+ pub const OUT_RATE_LIMIT: usize = 8;
13
+ pub const OUT_RATE_REMAINING: usize = 12;
14
+ pub const OUT_RATE_RESET: usize = 16;
15
+ pub const OUT_RETRY_AFTER: usize = 24;
16
+ pub const OUT_COOKIES_JSON_LEN: usize = 32;
17
+ pub const OUT_QUERY_JSON_LEN: usize = 36;
18
+ pub const OUT_HEADER_VARIANT: usize = 40;
19
+ pub const OUT_BODY_JSON_LEN: usize = 44;
20
+ pub const OUT_DATA_START: usize = 48;
21
+
22
+ // ── Flags ─────────────────────────────────────────────────────────
23
+ pub const FLAG_HAS_COOKIES: u32 = 1 << 0;
24
+ pub const FLAG_HAS_QUERY: u32 = 1 << 1;
25
+ pub const FLAG_BODY_VALID_JSON: u32 = 1 << 2;
26
+ pub const FLAG_SCHEMA_VALID: u32 = 1 << 3;
27
+ pub const FLAG_CORS_ALLOWED: u32 = 1 << 4;
28
+ pub const FLAG_IS_PREFLIGHT: u32 = 1 << 5;
29
+ pub const FLAG_RATE_LIMITED: u32 = 1 << 6;
30
+ pub const FLAG_HTTPS: u32 = 1 << 7;
31
+ pub const FLAG_TRUSTED_PROXY: u32 = 1 << 8;
32
+ pub const FLAG_BODY_TRUNCATED: u32 = 1 << 9;
33
+
34
+ // ── Header variant bits ───────────────────────────────────────────
35
+ pub const HV_JSON: u8 = 1 << 0;
36
+ pub const HV_CORS_SIMPLE: u8 = 1 << 1;
37
+ pub const HV_CORS_PREFLIGHT: u8 = 1 << 2;
38
+ pub const HV_RATE_ACTIVE: u8 = 1 << 3;
39
+ pub const HV_RATE_LIMITED: u8 = 1 << 4;
40
+
41
+ // ── Error codes ───────────────────────────────────────────────────
42
+ pub const ERR_CODE_NONE: u8 = 0;
43
+ pub const ERR_CODE_CORS_PREFLIGHT: u8 = 1;
44
+ pub const ERR_CODE_RATE_LIMITED: u8 = 2;
45
+ pub const ERR_CODE_BODY_TOO_LARGE: u8 = 3;
46
+ pub const ERR_CODE_INVALID_JSON: u8 = 4;
47
+ pub const ERR_CODE_SCHEMA_VALIDATION: u8 = 5;
48
+ pub const ERR_CODE_BAD_REQUEST: u8 = 6;
49
+ pub const ERR_CODE_REQUEST_TOO_LARGE: u8 = 7;
50
+
51
+ // ── Unsafe output helpers ─────────────────────────────────────────
52
+
53
+ /// Write a u16 at the given position in little-endian format.
54
+ #[inline(always)]
55
+ pub unsafe fn write_u16(out: &mut [u8], pos: usize, value: u16) {
56
+ ptr::copy_nonoverlapping(value.to_le_bytes().as_ptr(), out.as_mut_ptr().add(pos), 2);
57
+ }
58
+
59
+ /// Write a u32 at the given position in little-endian format.
60
+ #[inline(always)]
61
+ pub unsafe fn write_u32(out: &mut [u8], pos: usize, value: u32) {
62
+ ptr::copy_nonoverlapping(value.to_le_bytes().as_ptr(), out.as_mut_ptr().add(pos), 4);
63
+ }
64
+
65
+ /// Write a u64 at the given position in little-endian format.
66
+ #[inline(always)]
67
+ pub unsafe fn write_u64(out: &mut [u8], pos: usize, value: u64) {
68
+ ptr::copy_nonoverlapping(value.to_le_bytes().as_ptr(), out.as_mut_ptr().add(pos), 8);
69
+ }
70
+
71
+ // ── Slice cursor for sequential writing ───────────────────────────
72
+
73
+ /// A lightweight zero-cost cursor over a mutable byte slice.
74
+ /// Used for sequential writes without bounds checking (caller must ensure capacity).
75
+ pub struct Buf<'a> {
76
+ slice: &'a mut [u8],
77
+ pos: usize,
78
+ }
79
+
80
+ impl<'a> Buf<'a> {
81
+ /// Create a new cursor starting at the beginning of the given slice.
82
+ #[inline(always)]
83
+ pub fn new(slice: &'a mut [u8]) -> Self {
84
+ Self { slice, pos: 0 }
85
+ }
86
+
87
+ /// The current write position.
88
+ #[inline(always)]
89
+ pub fn pos(&self) -> usize {
90
+ self.pos
91
+ }
92
+
93
+ /// Returns true if there is enough remaining capacity.
94
+ #[inline(always)]
95
+ pub fn can_write(&self, n: usize) -> bool {
96
+ self.pos + n <= self.slice.len()
97
+ }
98
+
99
+ /// Advance the cursor by `n` bytes (caller must have written to them).
100
+ #[inline(always)]
101
+ pub fn advance(&mut self, n: usize) {
102
+ self.pos += n;
103
+ }
104
+
105
+ /// Write a single byte.
106
+ #[inline(always)]
107
+ pub fn push(&mut self, b: u8) {
108
+ self.slice[self.pos] = b;
109
+ self.pos += 1;
110
+ }
111
+
112
+ /// Write a byte slice.
113
+ #[inline(always)]
114
+ pub fn write(&mut self, bytes: &[u8]) {
115
+ let end = self.pos + bytes.len();
116
+ self.slice[self.pos..end].copy_from_slice(bytes);
117
+ self.pos = end;
118
+ }
119
+
120
+ /// Write a u8 at the current position.
121
+ #[inline(always)]
122
+ pub fn write_u8(&mut self, value: u8) {
123
+ self.slice[self.pos] = value;
124
+ self.pos += 1;
125
+ }
126
+
127
+ /// Write a u16 in little-endian at the current position.
128
+ #[inline(always)]
129
+ pub fn write_u16(&mut self, value: u16) {
130
+ let end = self.pos + 2;
131
+ self.slice[self.pos..end].copy_from_slice(&value.to_le_bytes());
132
+ self.pos = end;
133
+ }
134
+
135
+ /// Write a u32 in little-endian at the current position.
136
+ #[inline(always)]
137
+ pub fn write_u32(&mut self, value: u32) {
138
+ let end = self.pos + 4;
139
+ self.slice[self.pos..end].copy_from_slice(&value.to_le_bytes());
140
+ self.pos = end;
141
+ }
142
+
143
+ /// Write a u32 at a specific position (patch).
144
+ #[inline(always)]
145
+ pub fn patch_u32(&mut self, pos: usize, value: u32) {
146
+ self.slice[pos..pos + 4].copy_from_slice(&value.to_le_bytes());
147
+ }
148
+
149
+ /// Get a mutable sub-slice starting at current position.
150
+ #[inline(always)]
151
+ pub fn remaining_mut(&mut self) -> &mut [u8] {
152
+ &mut self.slice[self.pos..]
153
+ }
154
+
155
+ /// Get a sub-slice of the written area.
156
+ #[inline(always)]
157
+ pub fn written(&self) -> &[u8] {
158
+ &self.slice[..self.pos]
159
+ }
160
+ }
161
+
162
+ // ── Output header writer ──────────────────────────────────────────
163
+
164
+ /// Compute the header variant byte from booleans.
165
+ #[inline(always)]
166
+ pub fn compute_header_variant(
167
+ cors_ok: bool,
168
+ is_preflight: bool,
169
+ rate_active: bool,
170
+ rate_limited: bool,
171
+ wants_json: bool,
172
+ ) -> u8 {
173
+ let mut hv: u8 = 0;
174
+ if wants_json {
175
+ hv |= HV_JSON;
176
+ }
177
+ if cors_ok && !is_preflight {
178
+ hv |= HV_CORS_SIMPLE;
179
+ }
180
+ if is_preflight {
181
+ hv |= HV_CORS_PREFLIGHT;
182
+ }
183
+ if rate_active {
184
+ hv |= HV_RATE_ACTIVE;
185
+ }
186
+ if rate_limited {
187
+ hv |= HV_RATE_LIMITED;
188
+ }
189
+ hv
190
+ }
191
+
192
+ /// Write the output header into the buffer. Returns the total written bytes
193
+ /// (header + data payload sizes).
194
+ #[inline(always)]
195
+ pub fn write_output_header(
196
+ out: &mut [u8],
197
+ verdict: u8,
198
+ error_code: u8,
199
+ status: u16,
200
+ flags: u32,
201
+ rate_limit: u32,
202
+ rate_remaining: u32,
203
+ rate_reset_ms: u64,
204
+ retry_after_ms: u64,
205
+ cookies_json_len: u32,
206
+ query_json_len: u32,
207
+ header_variant: u8,
208
+ body_json_len: u32,
209
+ ) -> usize {
210
+ let status = if status == 101 || (200u16..=599u16).contains(&status) {
211
+ status
212
+ } else {
213
+ 500
214
+ };
215
+
216
+ unsafe {
217
+ out[OUT_VERDICT] = verdict;
218
+ out[OUT_ERROR_CODE] = error_code;
219
+ write_u16(out, OUT_STATUS, status);
220
+ write_u32(out, OUT_FLAGS, flags);
221
+ write_u32(out, OUT_RATE_LIMIT, rate_limit);
222
+ write_u32(out, OUT_RATE_REMAINING, rate_remaining);
223
+ write_u64(out, OUT_RATE_RESET, rate_reset_ms);
224
+ write_u64(out, OUT_RETRY_AFTER, retry_after_ms);
225
+ write_u32(out, OUT_COOKIES_JSON_LEN, cookies_json_len);
226
+ write_u32(out, OUT_QUERY_JSON_LEN, query_json_len);
227
+ out[OUT_HEADER_VARIANT] = header_variant;
228
+ out[OUT_HEADER_VARIANT + 1] = 0;
229
+ out[OUT_HEADER_VARIANT + 2] = 0;
230
+ out[OUT_HEADER_VARIANT + 3] = 0;
231
+ write_u32(out, OUT_BODY_JSON_LEN, body_json_len);
232
+ }
233
+
234
+ OUT_DATA_START
235
+ + cookies_json_len as usize
236
+ + query_json_len as usize
237
+ + body_json_len as usize
238
+ }
package/rust/proxy.rs ADDED
@@ -0,0 +1,90 @@
1
+ // rust/proxy.rs — Proxy trust, IP resolution, and HTTPS detection
2
+ // Extracted from ingress.rs for composability
3
+
4
+ use crate::headers::HeaderRefs;
5
+ use crate::ip_trust::ProxyTrustMode;
6
+ use crate::util::trim_ascii_whitespace;
7
+
8
+ /// Detect whether the request is HTTPS based on URL, proxy trust, and headers.
9
+ #[inline]
10
+ pub fn detect_https(
11
+ https_fixed: Option<bool>,
12
+ proxy_trust: &ProxyTrustMode,
13
+ url: &[u8],
14
+ headers: &HeaderRefs<'_>,
15
+ peer_trusted: bool,
16
+ ) -> bool {
17
+ if let Some(v) = https_fixed {
18
+ return v;
19
+ }
20
+
21
+ if url.starts_with(b"https://") || url.starts_with(b"wss://") {
22
+ return true;
23
+ }
24
+
25
+ if !proxy_trust.is_none() && peer_trusted && headers.has_xfp() {
26
+ if let Some(xfp) = headers.x_forwarded_proto() {
27
+ return trim_ascii_whitespace(xfp) == b"https";
28
+ }
29
+ }
30
+
31
+ false
32
+ }
33
+
34
+ /// Compute a rate-limiting key from the resolved IP.
35
+ #[inline]
36
+ pub fn rate_key_from_ip(ip: &crate::ip_trust::ResolvedIp, seed: u64) -> u64 {
37
+ ip.rate_key(seed)
38
+ }
39
+
40
+ // ── URL helpers ───────────────────────────────────────────────────
41
+
42
+ /// Extract the path component from a URL.
43
+ #[inline]
44
+ pub fn extract_path(url: &[u8]) -> &[u8] {
45
+ let search_start = if url.starts_with(b"https://") {
46
+ 8
47
+ } else if url.starts_with(b"http://") {
48
+ 7
49
+ } else {
50
+ 0
51
+ };
52
+
53
+ let search_space = &url[search_start..];
54
+
55
+ let path_start = match memchr::memchr(b'/', search_space) {
56
+ Some(i) => search_start + i,
57
+ None => return b"/",
58
+ };
59
+
60
+ let after_path = &url[path_start..];
61
+ let end = memchr::memchr2(b'?', b'#', after_path).unwrap_or(after_path.len());
62
+
63
+ &url[path_start..path_start + end]
64
+ }
65
+
66
+ /// Extract the query string from a URL (without the leading '?').
67
+ #[inline]
68
+ pub fn extract_query(url: &[u8]) -> &[u8] {
69
+ let search_start = if url.starts_with(b"https://") {
70
+ 8
71
+ } else if url.starts_with(b"http://") {
72
+ 7
73
+ } else {
74
+ 0
75
+ };
76
+
77
+ let search_space = &url[search_start..];
78
+
79
+ if let Some(i) = memchr::memchr2(b'?', b'#', search_space) {
80
+ if search_space[i] == b'?' {
81
+ let after_q = &search_space[i + 1..];
82
+ let frag = memchr::memchr(b'#', after_q).unwrap_or(after_q.len());
83
+ &after_q[..frag]
84
+ } else {
85
+ &[]
86
+ }
87
+ } else {
88
+ &[]
89
+ }
90
+ }
@@ -0,0 +1,141 @@
1
+ // rust/query_parser.rs — Unified zero-alloc query string parser
2
+ // Single _into_slice code path with _vec wrapper for callers needing Vec.
3
+ // Returns Cow<[u8]> when no decoding is needed (zero allocation).
4
+
5
+ use crate::util::{ensure_capacity, hex_val, write_bytes, write_u32_le};
6
+ use napi::bindgen_prelude::*;
7
+ use napi_derive::napi;
8
+ use std::borrow::Cow;
9
+
10
+ /// Write a single URL-decoded form component into the output slice.
11
+ #[inline]
12
+ fn write_decoded_form_component(src: &[u8], out: &mut [u8], pos: &mut usize) -> Result<()> {
13
+ let len_pos = *pos;
14
+ write_u32_le(out, pos, 0)?;
15
+ let start = *pos;
16
+
17
+ if memchr::memchr2(b'+', b'%', src).is_none() {
18
+ write_bytes(out, pos, src)?;
19
+ } else {
20
+ let mut i = 0usize;
21
+ while i < src.len() {
22
+ match src[i] {
23
+ b'+' => {
24
+ ensure_capacity(out, *pos, 1)?;
25
+ out[*pos] = b' ';
26
+ *pos += 1;
27
+ i += 1;
28
+ }
29
+ b'%' => {
30
+ if i + 2 >= src.len() {
31
+ return Err(Error::from_reason("invalid %-encoding: missing bytes"));
32
+ }
33
+ let hi = hex_val(src[i + 1])
34
+ .ok_or_else(|| Error::from_reason("invalid %-encoding: bad hi nibble"))?;
35
+ let lo = hex_val(src[i + 2])
36
+ .ok_or_else(|| Error::from_reason("invalid %-encoding: bad lo nibble"))?;
37
+ ensure_capacity(out, *pos, 1)?;
38
+ out[*pos] = (hi << 4) | lo;
39
+ *pos += 1;
40
+ i += 3;
41
+ }
42
+ b => {
43
+ ensure_capacity(out, *pos, 1)?;
44
+ out[*pos] = b;
45
+ *pos += 1;
46
+ i += 1;
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ let decoded_len = (*pos - start) as u32;
53
+ out[len_pos..len_pos + 4].copy_from_slice(&decoded_len.to_le_bytes());
54
+ Ok(())
55
+ }
56
+
57
+ /// Parse application/x-www-form-urlencoded into packed pairs.
58
+ /// Output: [u32 count] repeated { [u32 dec_name_len] [decoded_name] [u32 dec_val_len] [decoded_val] }
59
+ #[inline]
60
+ pub fn query_parse_packed_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
61
+ let mut pos = 0usize;
62
+ write_u32_le(out, &mut pos, 0)?;
63
+ let mut count = 0u32;
64
+
65
+ for pair in input.split(|&b| b == b'&') {
66
+ if pair.is_empty() { continue; }
67
+ let (key, value) = match pair.iter().position(|&b| b == b'=') {
68
+ Some(eq) => (&pair[..eq], &pair[eq + 1..]),
69
+ None => (pair, &[][..]),
70
+ };
71
+ write_decoded_form_component(key, out, &mut pos)?;
72
+ write_decoded_form_component(value, out, &mut pos)?;
73
+ count += 1;
74
+ }
75
+
76
+ out[0..4].copy_from_slice(&count.to_le_bytes());
77
+ Ok(pos)
78
+ }
79
+
80
+ /// Allocating parser — conservative upper bound, no pre-scan.
81
+ /// Returns `Cow::Owned(Vec<u8>)` when decoding is needed,
82
+ /// `Cow::Borrowed(&[u8])` when input can be used as-is (no % or +).
83
+ #[inline]
84
+ pub fn query_parse_packed_cow(input: &[u8]) -> Result<Cow<'_, [u8]>> {
85
+ // Fast path: if no decoding is needed at all (no % or + anywhere),
86
+ // we can return the original input borrowed as a single-item packed result.
87
+ if memchr::memchr2(b'+', b'%', input).is_none() && memchr::memchr(b'&', input).is_none() {
88
+ // No special chars: encode as packed [1 item: input]
89
+ // Actually, query string might still have structure. Let's check if it's a single key=value
90
+ if input.is_empty() {
91
+ return Ok(Cow::Borrowed(b"\0\0\0\0")); // empty packed
92
+ }
93
+ // We still need to produce packed format, so we allocate a minimal buffer
94
+ let mut out = Vec::with_capacity(input.len() + 12);
95
+ out.extend_from_slice(&1u32.to_le_bytes()); // count = 1
96
+ // key
97
+ if let Some(eq) = input.iter().position(|&b| b == b'=') {
98
+ let key = &input[..eq];
99
+ out.extend_from_slice(&(key.len() as u32).to_le_bytes());
100
+ out.extend_from_slice(key);
101
+ let val = &input[eq + 1..];
102
+ out.extend_from_slice(&(val.len() as u32).to_le_bytes());
103
+ out.extend_from_slice(val);
104
+ } else {
105
+ out.extend_from_slice(&(input.len() as u32).to_le_bytes());
106
+ out.extend_from_slice(input);
107
+ out.extend_from_slice(&0u32.to_le_bytes()); // empty value
108
+ }
109
+ return Ok(Cow::Owned(out));
110
+ }
111
+
112
+ // Needs decoding — use the standard path with upper-bound allocation
113
+ query_parse_packed_vec(input).map(Cow::Owned)
114
+ }
115
+
116
+ /// Allocating parser — conservative upper bound, no pre-scan.
117
+ #[inline]
118
+ pub fn query_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
119
+ // Packed output is larger than input: each component gets 4-byte length prefix.
120
+ // Upper bound: 4 (count) + input.len() * 2 * 5 ≈ 10x for worst-case (many small pairs).
121
+ // Conservative: 9x + 16 (same as before).
122
+ let upper_bound = input.len().saturating_mul(9).saturating_add(16);
123
+ let mut out = vec![0u8; upper_bound];
124
+ match query_parse_packed_into_slice(input, &mut out) {
125
+ Ok(written) => {
126
+ out.truncate(written);
127
+ Ok(out)
128
+ }
129
+ Err(e) => Err(e),
130
+ }
131
+ }
132
+
133
+ #[napi]
134
+ pub fn query_parse_packed(input: Uint8Array) -> Result<Buffer> {
135
+ Ok(Buffer::from(query_parse_packed_vec(input.as_ref())?))
136
+ }
137
+
138
+ #[napi]
139
+ pub fn query_parse_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
140
+ crate::util::run_packed_into(&input, &mut output, query_parse_packed_into_slice)
141
+ }
@@ -0,0 +1,52 @@
1
+ // rust/random_token.rs — v2: prefer stack buffer for common sizes
2
+ use napi::bindgen_prelude::*;
3
+ use napi_derive::napi;
4
+
5
+ const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
6
+
7
+ #[inline(always)]
8
+ fn hex_encode(bytes: &[u8], out: &mut [u8]) {
9
+ for (i, &b) in bytes.iter().enumerate() {
10
+ out[2 * i] = HEX_LOWER[(b >> 4) as usize];
11
+ out[2 * i + 1] = HEX_LOWER[(b & 0x0f) as usize];
12
+ }
13
+ }
14
+
15
+ #[napi]
16
+ pub fn random_token(byte_len: u32) -> Result<Buffer> {
17
+ let len = byte_len as usize;
18
+
19
+ // ⭐ Common sizes (16, 32, 64) use stack buffers — no heap alloc.
20
+ match len {
21
+ 16 => {
22
+ let mut bytes = [0u8; 16];
23
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
24
+ let mut out = [0u8; 32];
25
+ hex_encode(&bytes, &mut out);
26
+ Ok(Buffer::from(out.to_vec()))
27
+ }
28
+ 32 => {
29
+ let mut bytes = [0u8; 32];
30
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
31
+ let mut out = [0u8; 64];
32
+ hex_encode(&bytes, &mut out);
33
+ Ok(Buffer::from(out.to_vec()))
34
+ }
35
+ 64 => {
36
+ let mut bytes = [0u8; 64];
37
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
38
+ let mut out = [0u8; 128];
39
+ hex_encode(&bytes, &mut out);
40
+ Ok(Buffer::from(out.to_vec()))
41
+ }
42
+ _ => {
43
+ let out_len = len.checked_mul(2)
44
+ .ok_or_else(|| Error::from_reason("byte_len too large"))?;
45
+ let mut bytes = vec![0u8; len];
46
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
47
+ let mut out = vec![0u8; out_len];
48
+ hex_encode(&bytes, &mut out);
49
+ Ok(Buffer::from(out))
50
+ }
51
+ }
52
+ }