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.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// rust/payload/sse.rs — Server-Sent Events framing.
|
|
2
|
+
//
|
|
3
|
+
// Backend-framework feature: SSE response framing. Encodes a single SSE event
|
|
4
|
+
// per the WHATWG SSE spec: optional `id:`, `event:`, `retry:` lines followed by
|
|
5
|
+
// one `data:` line per input line and a terminating blank line. Pure byte work
|
|
6
|
+
// with no allocation beyond the output buffer.
|
|
7
|
+
|
|
8
|
+
use napi::bindgen_prelude::*;
|
|
9
|
+
use napi_derive::napi;
|
|
10
|
+
|
|
11
|
+
// ── Pure-Rust core ─────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/// Digits in the base-10 decimal form of a `u64` (for the `retry:` line —
|
|
14
|
+
/// avoids `retry.to_string()`'s heap allocation).
|
|
15
|
+
#[inline]
|
|
16
|
+
fn u64_digits(mut n: u64) -> usize {
|
|
17
|
+
let mut c = 1;
|
|
18
|
+
while n >= 10 {
|
|
19
|
+
n /= 10;
|
|
20
|
+
c += 1;
|
|
21
|
+
}
|
|
22
|
+
c
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Exact encoded size of one SSE event (no allocation).
|
|
26
|
+
#[inline]
|
|
27
|
+
pub fn encode_event_size(
|
|
28
|
+
event: Option<&str>,
|
|
29
|
+
data: &[u8],
|
|
30
|
+
id: Option<&str>,
|
|
31
|
+
retry: Option<u64>,
|
|
32
|
+
) -> usize {
|
|
33
|
+
let mut need = 0usize;
|
|
34
|
+
if let Some(id) = id {
|
|
35
|
+
need += 5 + id.len(); // "id: " + id + "\n"
|
|
36
|
+
}
|
|
37
|
+
if let Some(event) = event {
|
|
38
|
+
need += 8 + event.len(); // "event: " + event + "\n"
|
|
39
|
+
}
|
|
40
|
+
if let Some(retry) = retry {
|
|
41
|
+
need += 8 + u64_digits(retry); // "retry: " + digits + "\n"
|
|
42
|
+
}
|
|
43
|
+
// One `data: ` (6) + `\n` (1) per input line, the line bytes themselves,
|
|
44
|
+
// then the terminating blank `\n`.
|
|
45
|
+
let line_count = data.iter().filter(|&&b| b == b'\n').count() + 1;
|
|
46
|
+
need + line_count * 7 + data.len() + 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Encode one SSE event into `out`, returning bytes written. Zero-alloc (no
|
|
50
|
+
/// `retry.to_string()`, no output `Vec`). Errors (`&'static str`) if `out` is
|
|
51
|
+
/// too small — nothing is written on error.
|
|
52
|
+
pub fn encode_event_into_slice(
|
|
53
|
+
event: Option<&str>,
|
|
54
|
+
data: &[u8],
|
|
55
|
+
id: Option<&str>,
|
|
56
|
+
retry: Option<u64>,
|
|
57
|
+
out: &mut [u8],
|
|
58
|
+
) -> std::result::Result<usize, &'static str> {
|
|
59
|
+
let need = encode_event_size(event, data, id, retry);
|
|
60
|
+
if need > out.len() {
|
|
61
|
+
return Err("sse encode: output buffer too small");
|
|
62
|
+
}
|
|
63
|
+
let mut w = 0usize;
|
|
64
|
+
if let Some(id) = id {
|
|
65
|
+
out[w..w + 4].copy_from_slice(b"id: ");
|
|
66
|
+
w += 4;
|
|
67
|
+
out[w..w + id.len()].copy_from_slice(id.as_bytes());
|
|
68
|
+
w += id.len();
|
|
69
|
+
out[w] = b'\n';
|
|
70
|
+
w += 1;
|
|
71
|
+
}
|
|
72
|
+
if let Some(event) = event {
|
|
73
|
+
out[w..w + 7].copy_from_slice(b"event: ");
|
|
74
|
+
w += 7;
|
|
75
|
+
out[w..w + event.len()].copy_from_slice(event.as_bytes());
|
|
76
|
+
w += event.len();
|
|
77
|
+
out[w] = b'\n';
|
|
78
|
+
w += 1;
|
|
79
|
+
}
|
|
80
|
+
if let Some(retry) = retry {
|
|
81
|
+
out[w..w + 7].copy_from_slice(b"retry: ");
|
|
82
|
+
w += 7;
|
|
83
|
+
let mut digits = [0u8; 20];
|
|
84
|
+
let mut n = retry;
|
|
85
|
+
let mut i = 0;
|
|
86
|
+
loop {
|
|
87
|
+
digits[i] = b'0' + (n % 10) as u8;
|
|
88
|
+
n /= 10;
|
|
89
|
+
i += 1;
|
|
90
|
+
if n == 0 {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for &d in digits[..i].iter().rev() {
|
|
95
|
+
out[w] = d;
|
|
96
|
+
w += 1;
|
|
97
|
+
}
|
|
98
|
+
out[w] = b'\n';
|
|
99
|
+
w += 1;
|
|
100
|
+
}
|
|
101
|
+
for line in data.split(|&b| b == b'\n') {
|
|
102
|
+
out[w..w + 6].copy_from_slice(b"data: ");
|
|
103
|
+
w += 6;
|
|
104
|
+
out[w..w + line.len()].copy_from_slice(line);
|
|
105
|
+
w += line.len();
|
|
106
|
+
out[w] = b'\n';
|
|
107
|
+
w += 1;
|
|
108
|
+
}
|
|
109
|
+
out[w] = b'\n';
|
|
110
|
+
w += 1;
|
|
111
|
+
Ok(w)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Encode one SSE event. Multi-line `data` is emitted as repeated `data:` lines.
|
|
115
|
+
pub fn encode_event(
|
|
116
|
+
event: Option<&str>,
|
|
117
|
+
data: &[u8],
|
|
118
|
+
id: Option<&str>,
|
|
119
|
+
retry: Option<u64>,
|
|
120
|
+
) -> Vec<u8> {
|
|
121
|
+
let mut out = vec![0u8; encode_event_size(event, data, id, retry)];
|
|
122
|
+
let w = encode_event_into_slice(event, data, id, retry, &mut out)
|
|
123
|
+
.expect("output sized to the exact encoded length");
|
|
124
|
+
out.truncate(w);
|
|
125
|
+
out
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── NAPI entry points ──────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/// Encode a single SSE event → bytes.
|
|
131
|
+
#[napi]
|
|
132
|
+
pub fn sse_encode_event(
|
|
133
|
+
event: Option<String>,
|
|
134
|
+
data: Uint8Array,
|
|
135
|
+
id: Option<String>,
|
|
136
|
+
retry: Option<u32>,
|
|
137
|
+
) -> Buffer {
|
|
138
|
+
Buffer::from(encode_event(
|
|
139
|
+
event.as_deref(),
|
|
140
|
+
data.as_ref(),
|
|
141
|
+
id.as_deref(),
|
|
142
|
+
retry.map(u64::from),
|
|
143
|
+
))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Parallel SSE batch: packed `[u32 count]{[u32 len][data]}` in → packed
|
|
147
|
+
/// `[u32 count]{[u32 len][event]}` out (same event/id/retry for all items).
|
|
148
|
+
#[napi]
|
|
149
|
+
pub fn sse_encode_batch_packed(
|
|
150
|
+
data: Uint8Array,
|
|
151
|
+
event: Option<String>,
|
|
152
|
+
id: Option<String>,
|
|
153
|
+
retry: Option<u32>,
|
|
154
|
+
) -> Result<Buffer> {
|
|
155
|
+
let encode_one =
|
|
156
|
+
|d: &[u8]| encode_event(event.as_deref(), d, id.as_deref(), retry.map(u64::from));
|
|
157
|
+
crate::util::run_packed_batch(data.as_ref(), encode_one).map(Buffer::from)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[cfg(test)]
|
|
161
|
+
mod tests {
|
|
162
|
+
use super::*;
|
|
163
|
+
|
|
164
|
+
#[test]
|
|
165
|
+
fn encodes_basic_event() {
|
|
166
|
+
assert_eq!(encode_event(None, b"hello", None, None), b"data: hello\n\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
fn encodes_multiline_data() {
|
|
171
|
+
assert_eq!(
|
|
172
|
+
encode_event(None, b"line1\nline2", None, None),
|
|
173
|
+
b"data: line1\ndata: line2\n\n"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn encodes_named_event_with_id_and_retry() {
|
|
179
|
+
assert_eq!(
|
|
180
|
+
encode_event(Some("update"), b"payload", Some("42"), Some(3000)),
|
|
181
|
+
b"id: 42\nevent: update\nretry: 3000\ndata: payload\n\n"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn empty_data_still_terminates() {
|
|
187
|
+
assert_eq!(encode_event(None, b"", None, None), b"data: \n\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn into_slice_matches_allocating() {
|
|
192
|
+
type SseCase<'a> = (Option<&'a str>, &'a [u8], Option<&'a str>, Option<u64>);
|
|
193
|
+
let cases: &[SseCase] = &[
|
|
194
|
+
(None, b"hello", None, None),
|
|
195
|
+
(Some("update"), b"payload", Some("42"), Some(3000)),
|
|
196
|
+
(None, b"line1\nline2\nline3", None, None),
|
|
197
|
+
(Some("ev"), b"", Some("id"), Some(0)),
|
|
198
|
+
(Some(""), b"x", None, None),
|
|
199
|
+
];
|
|
200
|
+
for &(event, data, id, retry) in cases {
|
|
201
|
+
let expected = encode_event(event, data, id, retry);
|
|
202
|
+
let mut out = vec![0u8; expected.len() + 8];
|
|
203
|
+
let w = encode_event_into_slice(event, data, id, retry, &mut out).unwrap();
|
|
204
|
+
assert_eq!(w, expected.len());
|
|
205
|
+
assert_eq!(&out[..w], &expected[..]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[test]
|
|
210
|
+
fn into_slice_rejects_too_small() {
|
|
211
|
+
let mut out = [0u8; 3];
|
|
212
|
+
assert!(encode_event_into_slice(Some("update"), b"payload", None, None, &mut out).is_err());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// rust/payload/template.rs — HTML/text templating via minijinja.
|
|
2
|
+
//
|
|
3
|
+
// Backend-framework feature: server-side templating. minijinja is a fast, safe,
|
|
4
|
+
// Jinja2-compatible engine (no eval, no unsafe). Templates are compiled once and
|
|
5
|
+
// cached inside the `Environment`. The napi JS-object `render` accepts a
|
|
6
|
+
// `serde_json::Value` (marshal from JS); the pre-serialized byte paths
|
|
7
|
+
// (`render_bytes` / batch / C-ABI) parse straight into `sonic_rs::Value`
|
|
8
|
+
// (compact, zero per-key heap `String`) — both render via the `Serialize`
|
|
9
|
+
// trait, so behavior is identical.
|
|
10
|
+
//
|
|
11
|
+
// The napi class keeps an owned `Environment<'static>` (templates added via
|
|
12
|
+
// `add_template_owned`), so it is `Send + Sync` and safe to hold across calls.
|
|
13
|
+
|
|
14
|
+
use napi::bindgen_prelude::*;
|
|
15
|
+
use napi_derive::napi;
|
|
16
|
+
|
|
17
|
+
/// A compiled template renderer. Construct once per template, render many times
|
|
18
|
+
/// with different contexts.
|
|
19
|
+
#[napi]
|
|
20
|
+
pub struct TemplateRenderer {
|
|
21
|
+
// Owns the compiled template state (templates added via add_template_owned).
|
|
22
|
+
env: minijinja::Environment<'static>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[napi]
|
|
26
|
+
impl TemplateRenderer {
|
|
27
|
+
#[napi(constructor)]
|
|
28
|
+
pub fn new(source: String) -> Result<Self> {
|
|
29
|
+
let mut env = minijinja::Environment::new();
|
|
30
|
+
env.add_template_owned("main", source)
|
|
31
|
+
.map_err(|e| Error::from_reason(format!("template compile failed: {e}")))?;
|
|
32
|
+
Ok(Self { env })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Opaque handle to the compiled template, for the `bun:ffi` C-ABI fast
|
|
36
|
+
/// path (`castrum_template_render` in rust/ffi.rs). Only valid while THIS
|
|
37
|
+
/// instance is alive; the JS wrapper holds the instance.
|
|
38
|
+
#[napi]
|
|
39
|
+
pub fn inner_ptr(&self) -> u64 {
|
|
40
|
+
self as *const TemplateRenderer as u64
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Render the template with the given JSON context → UTF-8 bytes.
|
|
44
|
+
#[napi]
|
|
45
|
+
pub fn render(&self, context: serde_json::Value) -> Result<Buffer> {
|
|
46
|
+
let tpl = self
|
|
47
|
+
.env
|
|
48
|
+
.get_template("main")
|
|
49
|
+
.map_err(|e| Error::from_reason(format!("template compile failed: {e}")))?;
|
|
50
|
+
let out = tpl
|
|
51
|
+
.render(&context)
|
|
52
|
+
.map_err(|e| Error::from_reason(format!("template render failed: {e}")))?;
|
|
53
|
+
Ok(Buffer::from(out.into_bytes()))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Render the template with a PRE-SERIALIZED JSON context (bytes) → UTF-8
|
|
57
|
+
/// bytes. Avoids the napi `serde_json::Value` DOM marshal of `render` for
|
|
58
|
+
/// callers that already hold the context bytes (same core as the packed
|
|
59
|
+
/// batch path). The context is parsed straight into a `sonic_rs::Value`
|
|
60
|
+
/// (compact, no per-key heap `String`) instead of `serde_json::Value`.
|
|
61
|
+
#[napi]
|
|
62
|
+
pub fn render_bytes(&self, context_json: Uint8Array) -> Result<Buffer> {
|
|
63
|
+
let tpl = self
|
|
64
|
+
.env
|
|
65
|
+
.get_template("main")
|
|
66
|
+
.map_err(|e| Error::from_reason(format!("template compile failed: {e}")))?;
|
|
67
|
+
let context: sonic_rs::Value = sonic_rs::from_slice(context_json.as_ref())
|
|
68
|
+
.map_err(|e| Error::from_reason(format!("template context is not valid JSON: {e}")))?;
|
|
69
|
+
let out = tpl
|
|
70
|
+
.render(&context)
|
|
71
|
+
.map_err(|e| Error::from_reason(format!("template render failed: {e}")))?;
|
|
72
|
+
Ok(Buffer::from(out.into_bytes()))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Parallel render batch: packed `[u32 count]{[u32 len][context-json]}` in
|
|
76
|
+
/// → packed `[u32 count]{[u32 len][rendered]}` out. The compiled template
|
|
77
|
+
/// is fetched ONCE and reused for every item — no per-call recompilation.
|
|
78
|
+
#[napi]
|
|
79
|
+
pub fn render_batch_packed(&self, data: Uint8Array) -> Result<Buffer> {
|
|
80
|
+
// get_template is a cheap internal Arc lookup — the expensive part
|
|
81
|
+
// (compiling the template) happened once in the constructor.
|
|
82
|
+
let tpl = self
|
|
83
|
+
.env
|
|
84
|
+
.get_template("main")
|
|
85
|
+
.map_err(|e| Error::from_reason(format!("template compile failed: {e}")))?;
|
|
86
|
+
|
|
87
|
+
let render_one = |context_json: &[u8]| -> Vec<u8> {
|
|
88
|
+
let Ok(context) = sonic_rs::from_slice::<sonic_rs::Value>(context_json) else {
|
|
89
|
+
return Vec::new();
|
|
90
|
+
};
|
|
91
|
+
tpl.render(&context)
|
|
92
|
+
.map(|s| s.into_bytes())
|
|
93
|
+
.unwrap_or_default()
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
crate::util::run_packed_batch(data.as_ref(), render_one).map(Buffer::from)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// C-ABI support: render the compiled template with a pre-serialized JSON
|
|
101
|
+
/// context. `Err(())` on invalid JSON context / render error (napi parity).
|
|
102
|
+
///
|
|
103
|
+
/// # Safety
|
|
104
|
+
/// `p` must be a valid `*const TemplateRenderer` from `inner_ptr`, alive for
|
|
105
|
+
/// the call (the JS wrapper holds the napi instance).
|
|
106
|
+
pub(crate) unsafe fn template_render_core(
|
|
107
|
+
p: *const TemplateRenderer,
|
|
108
|
+
context_json: &[u8],
|
|
109
|
+
) -> std::result::Result<Vec<u8>, ()> {
|
|
110
|
+
let this = &*p;
|
|
111
|
+
let tpl = this.env.get_template("main").map_err(|_| ())?;
|
|
112
|
+
let context: sonic_rs::Value = sonic_rs::from_slice(context_json).map_err(|_| ())?;
|
|
113
|
+
let out = tpl.render(&context).map_err(|_| ())?;
|
|
114
|
+
Ok(out.into_bytes())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Parallel render batch: packed `[u32 count]{[u32 len][context-json]}` in →
|
|
118
|
+
/// packed `[u32 count]{[u32 len][rendered]}` out. A new `TemplateRenderer` is
|
|
119
|
+
/// built once per call from `source`, then all contexts render in parallel.
|
|
120
|
+
#[napi]
|
|
121
|
+
pub fn template_render_batch_packed(data: Uint8Array, source: String) -> Result<Buffer> {
|
|
122
|
+
let mut env = minijinja::Environment::new();
|
|
123
|
+
env.add_template_owned("main", source)
|
|
124
|
+
.map_err(|e| Error::from_reason(format!("template compile failed: {e}")))?;
|
|
125
|
+
|
|
126
|
+
let render_one = |context_json: &[u8]| -> Vec<u8> {
|
|
127
|
+
let Ok(context) = sonic_rs::from_slice::<sonic_rs::Value>(context_json) else {
|
|
128
|
+
return Vec::new();
|
|
129
|
+
};
|
|
130
|
+
let Ok(tpl) = env.get_template("main") else {
|
|
131
|
+
return Vec::new();
|
|
132
|
+
};
|
|
133
|
+
tpl.render(&context)
|
|
134
|
+
.map(|s| s.into_bytes())
|
|
135
|
+
.unwrap_or_default()
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
crate::util::run_packed_batch(data.as_ref(), render_one).map(Buffer::from)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[cfg(test)]
|
|
142
|
+
mod tests {
|
|
143
|
+
use super::*;
|
|
144
|
+
use std::collections::BTreeMap;
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn renders_variables() {
|
|
148
|
+
let mut env = minijinja::Environment::new();
|
|
149
|
+
env.add_template("t", "Hello {{ name }}!").unwrap();
|
|
150
|
+
let tpl = env.get_template("t").unwrap();
|
|
151
|
+
let mut ctx = BTreeMap::new();
|
|
152
|
+
ctx.insert("name", "World");
|
|
153
|
+
assert_eq!(tpl.render(ctx).unwrap(), "Hello World!");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn renders_loops_over_serde_value() {
|
|
158
|
+
let mut env = minijinja::Environment::new();
|
|
159
|
+
env.add_template(
|
|
160
|
+
"t",
|
|
161
|
+
"{% for u in users %}<li>{{ u.name }} ({{ u.id }})</li>{% endfor %}",
|
|
162
|
+
)
|
|
163
|
+
.unwrap();
|
|
164
|
+
let tpl = env.get_template("t").unwrap();
|
|
165
|
+
|
|
166
|
+
let context = serde_json::json!({
|
|
167
|
+
"users": [
|
|
168
|
+
{ "name": "Alice", "id": 1 },
|
|
169
|
+
{ "name": "Bob", "id": 2 },
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
let out = tpl.render(&context).unwrap();
|
|
173
|
+
assert_eq!(out, "<li>Alice (1)</li><li>Bob (2)</li>");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn compile_error_is_reported() {
|
|
178
|
+
let mut env = minijinja::Environment::new();
|
|
179
|
+
assert!(env.add_template("t", "{% for x in %}").is_err());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn missing_variable_renders_empty() {
|
|
184
|
+
let mut env = minijinja::Environment::new();
|
|
185
|
+
env.add_template("t", "[{{ missing }}]").unwrap();
|
|
186
|
+
let tpl = env.get_template("t").unwrap();
|
|
187
|
+
let ctx = serde_json::json!({});
|
|
188
|
+
assert_eq!(tpl.render(&ctx).unwrap(), "[]");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn pack_contexts(contexts: &[serde_json::Value]) -> Vec<u8> {
|
|
192
|
+
let mut out = Vec::new();
|
|
193
|
+
out.extend_from_slice(&(contexts.len() as u32).to_le_bytes());
|
|
194
|
+
for c in contexts {
|
|
195
|
+
let bytes = serde_json::to_vec(c).unwrap();
|
|
196
|
+
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
|
197
|
+
out.extend_from_slice(&bytes);
|
|
198
|
+
}
|
|
199
|
+
out
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn renderer_instance_reuses_compiled_template() {
|
|
204
|
+
let r = TemplateRenderer::new("Hello {{ name }}!".to_string()).unwrap();
|
|
205
|
+
let ctx = serde_json::json!({ "name": "World" });
|
|
206
|
+
assert_eq!(&r.render(ctx).unwrap()[..], b"Hello World!");
|
|
207
|
+
|
|
208
|
+
// Batch path must reuse the SAME compiled template.
|
|
209
|
+
let data = pack_contexts(&[
|
|
210
|
+
serde_json::json!({ "name": "Alice" }),
|
|
211
|
+
serde_json::json!({ "name": "Bob" }),
|
|
212
|
+
]);
|
|
213
|
+
let batch = r.render_batch_packed(Uint8Array::new(data)).unwrap();
|
|
214
|
+
assert_eq!(u32::from_le_bytes(batch[..4].try_into().unwrap()), 2);
|
|
215
|
+
|
|
216
|
+
let mut pos = 4usize;
|
|
217
|
+
let mut renders = Vec::new();
|
|
218
|
+
for _ in 0..2 {
|
|
219
|
+
let len = u32::from_le_bytes(batch[pos..pos + 4].try_into().unwrap()) as usize;
|
|
220
|
+
pos += 4;
|
|
221
|
+
renders.push(String::from_utf8(batch[pos..pos + len].to_vec()).unwrap());
|
|
222
|
+
pos += len;
|
|
223
|
+
}
|
|
224
|
+
assert_eq!(
|
|
225
|
+
renders,
|
|
226
|
+
vec!["Hello Alice!".to_string(), "Hello Bob!".to_string()]
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn render_bytes_matches_render() {
|
|
232
|
+
let r = TemplateRenderer::new("Hello {{ name }}!".to_string()).unwrap();
|
|
233
|
+
let via_value = r.render(serde_json::json!({ "name": "World" })).unwrap();
|
|
234
|
+
let via_bytes = r
|
|
235
|
+
.render_bytes(Uint8Array::new(br#"{"name":"World"}"#.to_vec()))
|
|
236
|
+
.unwrap();
|
|
237
|
+
assert_eq!(via_value.as_ref(), via_bytes.as_ref());
|
|
238
|
+
assert_eq!(&via_bytes[..], b"Hello World!");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn render_bytes_rejects_invalid_json_context() {
|
|
243
|
+
let r = TemplateRenderer::new("x".to_string()).unwrap();
|
|
244
|
+
assert!(r
|
|
245
|
+
.render_bytes(Uint8Array::new(b"not json".to_vec()))
|
|
246
|
+
.is_err());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn renderer_rejects_bad_compile() {
|
|
251
|
+
assert!(TemplateRenderer::new("{% for x in %}".to_string()).is_err());
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// rust/payload/websocket.rs — RFC 6455 Sec-WebSocket-Accept key derivation.
|
|
2
|
+
|
|
3
|
+
use aws_lc_rs::digest;
|
|
4
|
+
use base64::engine::general_purpose::STANDARD as BASE64;
|
|
5
|
+
use base64::Engine;
|
|
6
|
+
use napi::bindgen_prelude::*;
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
|
|
9
|
+
const WS_MAGIC: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
10
|
+
|
|
11
|
+
/// Zero-alloc core: RFC 6455 Sec-WebSocket-Accept (exactly 28 bytes) into a
|
|
12
|
+
/// caller-provided buffer. Shared by the C-ABI (`bun:ffi`) path so it never
|
|
13
|
+
/// allocates the accept string.
|
|
14
|
+
pub fn ws_accept_key_into(key: &[u8], out: &mut [u8]) -> Result<usize> {
|
|
15
|
+
let mut ctx = digest::Context::new(&digest::SHA1_FOR_LEGACY_USE_ONLY);
|
|
16
|
+
ctx.update(key);
|
|
17
|
+
ctx.update(WS_MAGIC);
|
|
18
|
+
|
|
19
|
+
let hash = ctx.finish();
|
|
20
|
+
|
|
21
|
+
BASE64
|
|
22
|
+
.encode_slice(hash.as_ref(), out)
|
|
23
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// RFC 6455 Sec-WebSocket-Accept core over a raw key slice. Shared by the
|
|
27
|
+
/// scalar napi path and the packed batch path.
|
|
28
|
+
pub fn ws_accept_key_bytes(key: &[u8]) -> Result<Vec<u8>> {
|
|
29
|
+
let mut out = [0u8; 28];
|
|
30
|
+
let n = ws_accept_key_into(key, &mut out)?;
|
|
31
|
+
Ok(out[..n].to_vec())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[napi]
|
|
35
|
+
pub fn ws_accept_key(key: Uint8Array) -> Result<Buffer> {
|
|
36
|
+
Ok(Buffer::from(ws_accept_key_bytes(key.as_ref())?))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Packed WebSocket accept-key batch: `[u32 count]{[u32 len][key]}` in →
|
|
40
|
+
/// `[u32 count]{[u32 len][accept]}` out.
|
|
41
|
+
#[napi]
|
|
42
|
+
pub fn ws_accept_key_batch_packed(input: Uint8Array) -> Result<Buffer> {
|
|
43
|
+
crate::util::run_packed_batch(input.as_ref(), |k| {
|
|
44
|
+
ws_accept_key_bytes(k).unwrap_or_default()
|
|
45
|
+
})
|
|
46
|
+
.map(Buffer::from)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[cfg(test)]
|
|
50
|
+
mod tests {
|
|
51
|
+
use super::*;
|
|
52
|
+
|
|
53
|
+
#[test]
|
|
54
|
+
fn rfc6455_accept_key_vector() {
|
|
55
|
+
// RFC 6455 §1.3: accept = base64(SHA1(key + magic GUID)) where `key` is
|
|
56
|
+
// the Sec-WebSocket-Key header value (the base64 string) concatenated
|
|
57
|
+
// with the magic GUID. Verified against node:crypto.
|
|
58
|
+
let key = b"dGhlIHNhbXBsZSBub25jZQ==";
|
|
59
|
+
let out = ws_accept_key(Uint8Array::new(key.to_vec())).unwrap();
|
|
60
|
+
assert_eq!(out.as_ref(), b"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn different_keys_differ() {
|
|
65
|
+
let a = ws_accept_key(Uint8Array::new(b"AAAA".to_vec())).unwrap();
|
|
66
|
+
let b = ws_accept_key(Uint8Array::new(b"BBBB".to_vec())).unwrap();
|
|
67
|
+
assert_ne!(a.as_ref(), b.as_ref());
|
|
68
|
+
}
|
|
69
|
+
}
|