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
@@ -0,0 +1,107 @@
1
+ // rust/validation.rs — v2: memchr for IPv4 dots
2
+
3
+ #[cfg(not(feature = "fast-email"))]
4
+ use email_address::EmailAddress;
5
+
6
+ use napi::bindgen_prelude::*;
7
+ use napi_derive::napi;
8
+ use std::net::Ipv6Addr;
9
+
10
+ #[cfg(feature = "fast-email")]
11
+ #[inline]
12
+ fn email_is_valid(s: &str) -> bool { fast_chemail::is_valid_email(s) }
13
+
14
+ #[cfg(not(feature = "fast-email"))]
15
+ #[inline]
16
+ fn email_is_valid(s: &str) -> bool { EmailAddress::is_valid(s) }
17
+
18
+ #[inline]
19
+ pub fn validate_email_bytes(input: &[u8]) -> bool {
20
+ std::str::from_utf8(input).map(email_is_valid).unwrap_or(false)
21
+ }
22
+
23
+ #[inline]
24
+ pub fn validate_uuid_bytes(input: &[u8]) -> bool {
25
+ let Ok(b) = <&[u8; 36]>::try_from(input) else { return false; };
26
+
27
+ // Unroll the dash positions check.
28
+ if b[8] != b'-' || b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
29
+ return false;
30
+ }
31
+ if b[14] != b'4' { return false; }
32
+ if !matches!(b[19], b'8' | b'9' | b'a' | b'b' | b'A' | b'B') { return false; }
33
+
34
+ // Validate hex digits at all other positions.
35
+ // Use a lookup table for speed.
36
+ const HEX: [bool; 256] = {
37
+ let mut t = [false; 256];
38
+ let mut i = 0;
39
+ while i < 256 {
40
+ t[i] = matches!(i as u8, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F');
41
+ i += 1;
42
+ }
43
+ t
44
+ };
45
+
46
+ let mut i = 0usize;
47
+ while i < 36 {
48
+ if i == 8 || i == 13 || i == 18 || i == 23 { i += 1; continue; }
49
+ if i == 14 || i == 19 { i += 1; continue; }
50
+ if !HEX[b[i] as usize] { return false; }
51
+ i += 1;
52
+ }
53
+ true
54
+ }
55
+
56
+ #[inline]
57
+ fn is_valid_ipv4(b: &[u8]) -> bool {
58
+ if b.is_empty() || b.len() > 15 { return false; }
59
+
60
+ let mut parts = 0u8;
61
+ let mut start = 0usize;
62
+
63
+ // Use memchr to find dots faster.
64
+ for dot_idx in b.iter().enumerate().filter_map(|(i, &c)| if c == b'.' { Some(i) } else { None }) {
65
+ let part = &b[start..dot_idx];
66
+ if !validate_ipv4_part(part) { return false; }
67
+ parts += 1;
68
+ start = dot_idx + 1;
69
+ }
70
+
71
+ // Last part.
72
+ if !validate_ipv4_part(&b[start..]) { return false; }
73
+ parts += 1;
74
+
75
+ parts == 4
76
+ }
77
+
78
+ #[inline(always)]
79
+ fn validate_ipv4_part(part: &[u8]) -> bool {
80
+ if part.is_empty() || part.len() > 3 { return false; }
81
+ if part.len() > 1 && part[0] == b'0' { return false; }
82
+
83
+ let mut n = 0u16;
84
+ for &c in part {
85
+ if !c.is_ascii_digit() { return false; }
86
+ n = n * 10 + u16::from(c - b'0');
87
+ }
88
+ n <= 255
89
+ }
90
+
91
+ #[inline]
92
+ pub fn validate_ipv4_bytes(input: &[u8]) -> bool {
93
+ is_valid_ipv4(input)
94
+ }
95
+
96
+ #[inline]
97
+ pub fn validate_ipv6_bytes(input: &[u8]) -> bool {
98
+ simdutf8::basic::from_utf8(input)
99
+ .ok()
100
+ .and_then(|s| s.parse::<Ipv6Addr>().ok())
101
+ .is_some()
102
+ }
103
+
104
+ #[napi] pub fn validate_email(input: Uint8Array) -> bool { validate_email_bytes(input.as_ref()) }
105
+ #[napi] pub fn validate_uuid(input: Uint8Array) -> bool { validate_uuid_bytes(input.as_ref()) }
106
+ #[napi] pub fn validate_ipv4(input: Uint8Array) -> bool { validate_ipv4_bytes(input.as_ref()) }
107
+ #[napi] pub fn validate_ipv6(input: Uint8Array) -> bool { validate_ipv6_bytes(input.as_ref()) }
@@ -0,0 +1,23 @@
1
+ use base64::engine::general_purpose::STANDARD as BASE64;
2
+ use base64::Engine;
3
+ use napi::bindgen_prelude::*;
4
+ use napi_derive::napi;
5
+ use aws_lc_rs::digest;
6
+
7
+ const WS_MAGIC: &[u8] = b"258EAFA5-E914-47DA-95CA-5AB5DC11BE85";
8
+
9
+ #[napi]
10
+ pub fn ws_accept_key(key: Uint8Array) -> Result<Buffer> {
11
+ let mut ctx = digest::Context::new(&digest::SHA1_FOR_LEGACY_USE_ONLY);
12
+ ctx.update(key.as_ref());
13
+ ctx.update(WS_MAGIC);
14
+
15
+ let hash = ctx.finish();
16
+
17
+ let mut out = [0u8; 28];
18
+ let n = BASE64
19
+ .encode_slice(hash.as_ref(), &mut out)
20
+ .map_err(|e| Error::from_reason(e.to_string()))?;
21
+
22
+ Ok(Buffer::from(out[..n].to_vec()))
23
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./tasks/json";
2
+ export * from "./tasks/http";
3
+ export * from "./tasks/query";
4
+ export * from "./tasks/cookie";
5
+ export * from "./tasks/token";
6
+ export * from "./tasks/websocket";
7
+ export * from "./tasks/json-patch";
8
+ export * from "./tasks/hmac";
9
+ export * from "./tasks/validation";
10
+ export * from "./tasks/hashing";
11
+ export * from "./tasks/mime";
12
+ export * from "./tasks/url";
@@ -0,0 +1,22 @@
1
+ import { decoder } from "../../shared/bytes";
2
+ import { packPairs } from "../../shared/packed";
3
+
4
+ export function nativeCookieParsePacked(bytes: Uint8Array): Uint8Array {
5
+ const text = decoder.decode(bytes);
6
+ const pairs: Array<[string, string]> = [];
7
+
8
+ for (const rawPair of text.split(";")) {
9
+ const pair = rawPair.trim();
10
+ if (pair.length === 0) continue;
11
+
12
+ const eq = pair.indexOf("=");
13
+ const name = (eq >= 0 ? pair.slice(0, eq) : pair).trim();
14
+ const value = eq >= 0 ? pair.slice(eq + 1).trim() : "";
15
+
16
+ if (name.length === 0) continue;
17
+
18
+ pairs.push([name, value]);
19
+ }
20
+
21
+ return packPairs(pairs);
22
+ }
@@ -0,0 +1,18 @@
1
+ import * as CRC32 from "crc-32";
2
+
3
+ export function nativeCrc32(bytes: Uint8Array): number {
4
+ return CRC32.buf(bytes) >>> 0;
5
+ }
6
+
7
+ export function nativeFnv1a64(bytes: Uint8Array): bigint {
8
+ let hash = 0xcbf29ce484222325n;
9
+ const prime = 0x100000001b3n;
10
+ const mask = 0xffffffffffffffffn;
11
+
12
+ for (const byte of bytes) {
13
+ hash ^= BigInt(byte);
14
+ hash = (hash * prime) & mask;
15
+ }
16
+
17
+ return hash;
18
+ }
@@ -0,0 +1,38 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ import { decoder, encoder } from "../../shared/bytes";
4
+
5
+ export function nativeHmacSha256(
6
+ key: Uint8Array,
7
+ data: Uint8Array,
8
+ ): Uint8Array {
9
+ const hex = createHmac("sha256", Buffer.from(key))
10
+ .update(Buffer.from(data))
11
+ .digest("hex");
12
+
13
+ return encoder.encode(hex);
14
+ }
15
+
16
+ export function nativeHmacSha256Verify(
17
+ key: Uint8Array,
18
+ data: Uint8Array,
19
+ sig: Uint8Array,
20
+ ): boolean {
21
+ const expected = createHmac("sha256", Buffer.from(key))
22
+ .update(Buffer.from(data))
23
+ .digest();
24
+
25
+ const providedHex = decoder.decode(sig).trim();
26
+
27
+ if (!/^[0-9a-fA-F]*$/.test(providedHex)) {
28
+ return false;
29
+ }
30
+
31
+ const provided = Buffer.from(providedHex, "hex");
32
+
33
+ if (expected.length !== provided.length) {
34
+ return false;
35
+ }
36
+
37
+ return timingSafeEqual(expected, provided);
38
+ }
@@ -0,0 +1,62 @@
1
+ import { decoder, encoder } from "../../shared/bytes";
2
+
3
+
4
+
5
+ export function nativeHttpParseRequestPacked(bytes: Uint8Array): Uint8Array {
6
+ const text = decoder.decode(bytes);
7
+ const headerEnd = text.indexOf("\r\n\r\n");
8
+ const head = headerEnd >= 0 ? text.slice(0, headerEnd) : text;
9
+
10
+ const lines = head.split("\r\n");
11
+ const requestLine = lines[0] ?? "";
12
+ const [method = "", target = "", version = ""] = requestLine.split(" ");
13
+
14
+ const methodBytes = encoder.encode(method);
15
+ const pathBytes = encoder.encode(target);
16
+ const versionBytes = encoder.encode(version);
17
+
18
+ const headers: Array<[Uint8Array, Uint8Array]> = [];
19
+
20
+ for (const line of lines.slice(1)) {
21
+ const idx = line.indexOf(":");
22
+ if (idx > 0) {
23
+ const name = encoder.encode(line.slice(0, idx).trim().toLowerCase());
24
+ const value = encoder.encode(line.slice(idx + 1).trim());
25
+ headers.push([name, value]);
26
+ }
27
+ }
28
+
29
+ let total = 16;
30
+ total += methodBytes.byteLength;
31
+ total += pathBytes.byteLength;
32
+ total += versionBytes.byteLength;
33
+
34
+ for (const [name, value] of headers) {
35
+ total += 8 + name.byteLength + value.byteLength;
36
+ }
37
+
38
+ const out = new Uint8Array(total);
39
+ const dv = new DataView(out.buffer);
40
+ let offset = 0;
41
+
42
+ const writeBytes = (bytes: Uint8Array) => {
43
+ dv.setUint32(offset, bytes.byteLength, true);
44
+ offset += 4;
45
+ out.set(bytes, offset);
46
+ offset += bytes.byteLength;
47
+ };
48
+
49
+ writeBytes(methodBytes);
50
+ writeBytes(pathBytes);
51
+ writeBytes(versionBytes);
52
+
53
+ dv.setUint32(offset, headers.length, true);
54
+ offset += 4;
55
+
56
+ for (const [name, value] of headers) {
57
+ writeBytes(name);
58
+ writeBytes(value);
59
+ }
60
+
61
+ return out;
62
+ }
@@ -0,0 +1,12 @@
1
+ import { applyPatch, type Operation } from "fast-json-patch";
2
+ import { decoder, encoder } from "../../shared/bytes";
3
+
4
+ export function nativeJsonPatch(
5
+ docBytes: Uint8Array,
6
+ patchBytes: Uint8Array,
7
+ ): Uint8Array {
8
+ const doc = JSON.parse(decoder.decode(docBytes));
9
+ const patch = JSON.parse(decoder.decode(patchBytes)) as Operation[];
10
+ const result = applyPatch(doc, patch, true, false).newDocument;
11
+ return encoder.encode(JSON.stringify(result));
12
+ }
@@ -0,0 +1,50 @@
1
+ // src/baseline/tasks/json.ts
2
+ import { decoder, encoder } from "../../shared/bytes";
3
+
4
+ /**
5
+ * Older Bun builds accepted Uint8Array directly in JSON.parse().
6
+ * Bun 1.4 canary builds may reject it.
7
+ *
8
+ * We feature-detect once. If byte parsing is supported, use it.
9
+ * Otherwise fall back to standard UTF-8 decode + JSON.parse.
10
+ */
11
+ const JSON_PARSE_BYTES_SUPPORTED: boolean = (() => {
12
+ try {
13
+ JSON.parse(encoder.encode('{"a":1}') as any);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ })();
19
+
20
+ const parseJsonBytes: (bytes: Uint8Array) => unknown =
21
+ JSON_PARSE_BYTES_SUPPORTED
22
+ ? (bytes) => JSON.parse(bytes as any)
23
+ : (bytes) => JSON.parse(decoder.decode(bytes));
24
+
25
+ export function nativeJsonValid(bytes: Uint8Array): boolean {
26
+ try {
27
+ parseJsonBytes(bytes);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ export function nativeJsonSum(bytes: Uint8Array): bigint {
35
+ const parsed = parseJsonBytes(bytes);
36
+
37
+ if (!Array.isArray(parsed)) {
38
+ return 0n;
39
+ }
40
+
41
+ let sum = 0n;
42
+
43
+ for (const row of parsed as Array<{ id?: unknown }>) {
44
+ if (typeof row.id === "number" && Number.isFinite(row.id)) {
45
+ sum += BigInt(Math.trunc(row.id));
46
+ }
47
+ }
48
+
49
+ return sum;
50
+ }
@@ -0,0 +1,5 @@
1
+ import mime from "mime-types";
2
+
3
+ export function nativeMimeFromExtension(ext: string): string {
4
+ return mime.lookup(ext) || "application/octet-stream";
5
+ }
@@ -0,0 +1,14 @@
1
+ import { decoder } from "../../shared/bytes";
2
+ import { packPairs } from "../../shared/packed";
3
+
4
+ export function nativeQueryParsePacked(bytes: Uint8Array): Uint8Array {
5
+ const query = decoder.decode(bytes);
6
+ const sp = new URLSearchParams(query);
7
+
8
+ const pairs: Array<[string, string]> = [];
9
+ for (const [key, value] of sp.entries()) {
10
+ pairs.push([key, value]);
11
+ }
12
+
13
+ return packPairs(pairs);
14
+ }
@@ -0,0 +1,15 @@
1
+ const HEX = "0123456789abcdef";
2
+
3
+ export function nativeRandomToken(byteLen: number): Uint8Array {
4
+ const bytes = new Uint8Array(byteLen);
5
+ crypto.getRandomValues(bytes);
6
+
7
+ const hex = new Uint8Array(byteLen * 2);
8
+
9
+ for (let i = 0; i < byteLen; i++) {
10
+ hex[i * 2] = HEX.charCodeAt(bytes[i]! >> 4);
11
+ hex[i * 2 + 1] = HEX.charCodeAt(bytes[i]! & 0x0f);
12
+ }
13
+
14
+ return hex;
15
+ }
@@ -0,0 +1,11 @@
1
+ import { decoder } from "../../shared/bytes";
2
+
3
+ export function nativeUrlEncode(input: string | Uint8Array): string {
4
+ const text = typeof input === "string" ? input : decoder.decode(input);
5
+ return encodeURIComponent(text);
6
+ }
7
+
8
+ export function nativeUrlDecode(input: string | Uint8Array): string {
9
+ const text = typeof input === "string" ? input : decoder.decode(input);
10
+ return decodeURIComponent(text);
11
+ }
@@ -0,0 +1,19 @@
1
+ import { isIP } from "node:net";
2
+ import validator from "validator";
3
+ import { decoder } from "../../shared/bytes";
4
+
5
+ export function nativeValidateEmail(bytes: Uint8Array): boolean {
6
+ return validator.isEmail(decoder.decode(bytes));
7
+ }
8
+
9
+ export function nativeValidateUuid(bytes: Uint8Array): boolean {
10
+ return validator.isUUID(decoder.decode(bytes), 4);
11
+ }
12
+
13
+ export function nativeValidateIpv4(bytes: Uint8Array): boolean {
14
+ return validator.isIP(decoder.decode(bytes), 4);
15
+ }
16
+
17
+ export function nativeValidateIpv6(bytes: Uint8Array): boolean {
18
+ return isIP(decoder.decode(bytes)) === 6;
19
+ }
@@ -0,0 +1,14 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { decoder, encoder, toPlainBuffer } from "../../shared/bytes";
3
+
4
+ export function nativeWsAcceptKey(key: string | Uint8Array): Uint8Array {
5
+ const keyText = typeof key === "string" ? key : decoder.decode(key);
6
+ const magic = "258EAFA5-E914-47DA-95CA-5AB5DC11BE85";
7
+ const combined = encoder.encode(keyText + magic);
8
+
9
+ const hash = new Bun.CryptoHasher("sha1")
10
+ .update(toPlainBuffer(combined))
11
+ .digest();
12
+
13
+ return encoder.encode(Buffer.from(hash).toString("base64"));
14
+ }
@@ -0,0 +1,70 @@
1
+ import { decoder } from "../shared/bytes";
2
+ import { sortKeys } from "../shared/json";
3
+
4
+ export function parseJsonBytes(bytes: Uint8Array): unknown {
5
+ return JSON.parse(decoder.decode(bytes));
6
+ }
7
+
8
+ function typeName(value: unknown): string {
9
+ if (value === null) return "null";
10
+ if (Array.isArray(value)) return "array";
11
+ if (value instanceof Uint8Array) return "Uint8Array";
12
+ return typeof value;
13
+ }
14
+
15
+ /**
16
+ * Some Bun/NAPI builds return Rust i64/u64 as number when the value fits
17
+ * inside Number.MAX_SAFE_INTEGER, while baseline code returns bigint.
18
+ *
19
+ * For benchmark correctness checks, integer number and bigint should be
20
+ * treated as equal when their numeric value is the same.
21
+ */
22
+ function normalizeScalar(value: unknown): unknown {
23
+ if (typeof value === "bigint") {
24
+ return value;
25
+ }
26
+
27
+ if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value)) {
28
+ return BigInt(Math.trunc(value));
29
+ }
30
+
31
+ return value;
32
+ }
33
+
34
+ export function assertEqual(
35
+ actual: unknown,
36
+ expected: unknown,
37
+ label: string,
38
+ ): void {
39
+ const a = normalizeScalar(actual);
40
+ const b = normalizeScalar(expected);
41
+
42
+ if (a !== b) {
43
+ console.error(`FAIL: ${label}`);
44
+ console.error(
45
+ ` actual: ${String(actual)} (${typeName(actual)})`,
46
+ );
47
+ console.error(
48
+ ` expected: ${String(expected)} (${typeName(expected)})`,
49
+ );
50
+ process.exit(1);
51
+ throw new Error(`Assertion failed: ${label}`);
52
+ }
53
+ }
54
+
55
+ export function assertDeepEqual(
56
+ actual: unknown,
57
+ expected: unknown,
58
+ label: string,
59
+ ): void {
60
+ const a = JSON.stringify(sortKeys(actual));
61
+ const b = JSON.stringify(sortKeys(expected));
62
+
63
+ if (a !== b) {
64
+ console.error(`FAIL: ${label}`);
65
+ console.error(` actual: ${a}`);
66
+ console.error(` expected: ${b}`);
67
+ process.exit(1);
68
+ throw new Error(`Assertion failed: ${label}`);
69
+ }
70
+ }