castrum 0.9.4 → 0.9.7
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 +28 -44
- package/Cargo.toml +21 -4
- package/README.md +8 -6
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +4460 -3648
- package/docs/API.md +1 -0
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CASE_STUDY.md +4 -2
- package/docs/ENVIRONMENT.md +9 -2
- package/docs/FFI_BUN_GUIDE.md +166 -9
- package/docs/HOW_TO_ADD_AN_OP.md +34 -0
- package/docs/REPO_MAP.md +16 -5
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +5 -0
- package/package.json +172 -163
- package/rust/crypto/pbkdf2.rs +9 -0
- package/rust/crypto/session.rs +164 -7
- package/rust/ffi/crypto.rs +119 -29
- package/rust/ffi/mod.rs +1 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +173 -0
- package/rust/http/form.rs +6 -2
- package/rust/http/query_parser.rs +53 -22
- package/rust/ingress/native_route.rs +131 -140
- package/rust/ingress/pipeline.rs +3 -3
- package/rust/ingress/tests.rs +5 -4
- package/rust/json/json_ser.rs +13 -11
- package/rust/lib.rs +1 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/util/bytes.rs +238 -51
- package/src/ingress/health.ts +1 -2
- package/src/native/ffi/build/codecs.ts +29 -7
- package/src/native/ffi/build/compress.ts +8 -0
- package/src/native/ffi/build/instances.ts +54 -2
- package/src/native/ffi/build/metrics.ts +23 -7
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +18 -0
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +3 -1
- package/src/native/ffi/types.ts +57 -0
- package/src/native/ffi.ts +85 -14
- package/src/native/loader.ts +53 -3
- package/src/rust-ffi/scalar/http.ts +54 -16
- package/src/rust-ffi/text.ts +25 -3
- package/src/shared/bytes.ts +19 -0
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
## Methodology
|
|
9
9
|
|
|
10
|
-
- **Runtime**: Bun **1.4.0** (the
|
|
10
|
+
- **Runtime**: Bun **1.4.0** (the measurement anchor for the rows below; the
|
|
11
|
+
local toolchain has since moved to **1.4.2**, and these TIMING rows have not
|
|
12
|
+
been re-measured — only re-verified for parity, which
|
|
13
|
+
`test/unit/contract/delegation.test.ts` does on every run).
|
|
11
14
|
- **Addon**: castrum **release** build (`bun run build`, baseline CPU — the
|
|
12
15
|
shipped profile).
|
|
13
16
|
- **Driver**: `bun run check` (CPU benchmark → `bench/results/cpu/latest.json`).
|
package/index.ts
CHANGED
|
@@ -20,6 +20,11 @@ export * from './src/integration'
|
|
|
20
20
|
// dispatch single items, packed batches, and load() coalescing. See README.
|
|
21
21
|
export * from './src/loader'
|
|
22
22
|
export * from './src/rust-ffi'
|
|
23
|
+
// Off-thread native task runtime ("castrum Tasks"): submit a CPU-bound native
|
|
24
|
+
// op to the Rust pool and `await` it without ever blocking the JS thread.
|
|
25
|
+
// Bun-only offload today; Node runs the op synchronously. See
|
|
26
|
+
// docs/RND-CONCURRENCY.md.
|
|
27
|
+
export * from './src/task'
|
|
23
28
|
export type { OpDecision, OpImpl } from './src/selection'
|
|
24
29
|
// Native-vs-JS selection (OWNED HERE, not by consumers): `opImpl(op)` is the
|
|
25
30
|
// benchmark-driven recommendation for which operations should use the Rust
|
package/package.json
CHANGED
|
@@ -1,168 +1,177 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"name": "castrum",
|
|
3
|
+
"version": "0.9.7",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Enterprise-grade Bun + Rust runtime: Rust-accelerated FFI primitives, a production-grade HTTP ingress pipeline, validation, hashing, and JSON processing with benchmark-grade performance.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"bun",
|
|
13
|
+
"rust",
|
|
14
|
+
"napi",
|
|
15
|
+
"ffi",
|
|
16
|
+
"ingress",
|
|
17
|
+
"http",
|
|
18
|
+
"validation",
|
|
19
|
+
"hashing",
|
|
20
|
+
"json",
|
|
21
|
+
"performance",
|
|
22
|
+
"benchmark"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Axylabs/castrum.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/Axylabs/castrum#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/Axylabs/castrum/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"bun": "./index.ts",
|
|
39
|
+
"node": "./dist/index.js",
|
|
40
|
+
"default": "./dist/index.js"
|
|
27
41
|
},
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"bench:http:spike": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts spike",
|
|
102
|
-
"bench:http:soak": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts soak",
|
|
103
|
-
"bench:http:storm": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts storm",
|
|
104
|
-
"bench:http:boundary": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts boundary",
|
|
105
|
-
"bench:http:elysia": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts '' elysia",
|
|
106
|
-
"bench:http:ingress": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts '' ingress",
|
|
107
|
-
"bench:http:bun-only": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts '' bun",
|
|
108
|
-
"bench:http:all-heavy": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts 1",
|
|
109
|
-
"bench:http:ac": "bun bench/autocannon-stress.mjs",
|
|
110
|
-
"bench:http:stress:ac": "SCENARIO=03-stress bun bench/autocannon-stress.mjs",
|
|
111
|
-
"bench:startup": "bun run bench/startup.ts",
|
|
112
|
-
"bench:load": "bun run bench/http/load-phase.ts",
|
|
113
|
-
"bench:ffi": "bun run bench/ffi/ffi-all.ts",
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"index.ts",
|
|
46
|
+
"dist",
|
|
47
|
+
"src",
|
|
48
|
+
"docs",
|
|
49
|
+
"README.md",
|
|
50
|
+
"*.node",
|
|
51
|
+
"scripts/postinstall.mjs",
|
|
52
|
+
"rust",
|
|
53
|
+
"Cargo.toml",
|
|
54
|
+
"Cargo.lock",
|
|
55
|
+
"build.rs",
|
|
56
|
+
"rust-toolchain.toml",
|
|
57
|
+
"!src/bench",
|
|
58
|
+
"!src/baseline",
|
|
59
|
+
"!src/data"
|
|
60
|
+
],
|
|
61
|
+
"scripts": {
|
|
62
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
63
|
+
"build": "napi build --release --platform",
|
|
64
|
+
"build:perf": "bash scripts/build-perf.sh",
|
|
65
|
+
"build:v3": "bash scripts/build-v3.sh",
|
|
66
|
+
"build:all": "bun run build && bun run build:v3",
|
|
67
|
+
"build:js": "bun run build:js:bundle && bun run build:js:types",
|
|
68
|
+
"build:js:bundle": "bun build ./index.ts --target=node --format=esm --outdir=dist",
|
|
69
|
+
"build:js:types": "dts-bundle-generator -o dist/index.d.ts index.ts",
|
|
70
|
+
"bench": "bun run build && bun bench.ts",
|
|
71
|
+
"check": "bun bench.ts",
|
|
72
|
+
"test": "bun test test/unit test/property test/compat",
|
|
73
|
+
"test:all": "bun test && bun run typecheck && bun run typecheck:test && bun run lint:ci && bun run check:clean && cargo test --lib",
|
|
74
|
+
"test:coverage": "node scripts/check-coverage.mjs",
|
|
75
|
+
"setup": "bun install && bun run build",
|
|
76
|
+
"test:node": "node --test test/integration/node-smoke.test.mjs test/integration/node-enterprise.test.mjs",
|
|
77
|
+
"verify:install": "node scripts/verify-install.mjs",
|
|
78
|
+
"build:debug": "napi build --platform",
|
|
79
|
+
"lint": "bunx biome check .",
|
|
80
|
+
"lint:fix": "bunx biome check --write .",
|
|
81
|
+
"lint:ci": "bunx biome check . --formatter-enabled=false --assist-enabled=false --error-on-warnings",
|
|
82
|
+
"format": "bunx biome format --write .",
|
|
83
|
+
"typecheck": "bunx tsc --noEmit",
|
|
84
|
+
"typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
|
|
85
|
+
"test:rust": "cargo test",
|
|
86
|
+
"deny": "cargo deny check",
|
|
87
|
+
"check:version": "bun scripts/check-version.mjs",
|
|
88
|
+
"check:jsdoc": "bun scripts/check-jsdoc.ts",
|
|
89
|
+
"check:selection": "bun scripts/select-native.ts --check",
|
|
90
|
+
"check:clean": "bun scripts/check-clean.ts",
|
|
91
|
+
"gen:ai-map": "bun scripts/gen-ai-map.ts",
|
|
92
|
+
"audit": "bun audit",
|
|
93
|
+
"release": "bun scripts/release.ts",
|
|
94
|
+
"release:dry": "bun scripts/release.ts --dry-run",
|
|
95
|
+
"release:manual": "bun scripts/release.ts --publish",
|
|
96
|
+
"prepublishOnly": "bun run build:js && node scripts/prepublish.mjs",
|
|
97
|
+
"bench:http": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts",
|
|
98
|
+
"bench:http:smoke": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts smoke",
|
|
99
|
+
"bench:http:stress": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts stress",
|
|
100
|
+
"bench:http:heavy": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts heavy-json",
|
|
101
|
+
"bench:http:crud": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts crud",
|
|
102
|
+
"bench:http:spike": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts spike",
|
|
103
|
+
"bench:http:soak": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts soak",
|
|
104
|
+
"bench:http:storm": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts storm",
|
|
105
|
+
"bench:http:boundary": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts boundary",
|
|
106
|
+
"bench:http:elysia": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts '' elysia",
|
|
107
|
+
"bench:http:ingress": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts '' ingress",
|
|
108
|
+
"bench:http:bun-only": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts '' bun",
|
|
109
|
+
"bench:http:all-heavy": "BUN_CONFIG_MAX_HTTP_REQUESTS=65535 bun run bench/http/run-bench.ts 1",
|
|
110
|
+
"bench:http:ac": "bun bench/autocannon-stress.mjs",
|
|
111
|
+
"bench:http:stress:ac": "SCENARIO=03-stress bun bench/autocannon-stress.mjs",
|
|
112
|
+
"bench:startup": "bun run bench/startup.ts",
|
|
113
|
+
"bench:load": "bun run bench/http/load-phase.ts",
|
|
114
|
+
"bench:ffi": "bun run bench/ffi/ffi-all.ts",
|
|
114
115
|
"bench:newops": "bun bench/cost/rust-vs-js-new-ops.ts",
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
116
|
+
"bench:ffi:load": "bun run bench/ffi/ffi-load.ts",
|
|
117
|
+
"bench:ffi:public": "bun run bench/ffi/ffi-public.ts",
|
|
118
|
+
"bench:ffi:workers": "bun run bench/ffi/ffi-workers.ts",
|
|
119
|
+
"bench:margin": "bun run bench/ffi/ffi-margin.ts",
|
|
120
|
+
"bench:router": "bun run bench/cost/router-cost.ts",
|
|
121
|
+
"bench:task": "bun run bench/cost/task-offload.ts",
|
|
122
|
+
"bench:ingress-cost": "bun run bench/cost/ingress-cost.ts",
|
|
123
|
+
"bench:ingress-cost:post": "bun run bench/cost/ingress-cost-post.ts",
|
|
124
|
+
"verify:native:batch": "bun run scripts/verify-native-batch.ts"
|
|
125
|
+
},
|
|
126
|
+
"dependencies": {},
|
|
127
|
+
"peerDependencies": {
|
|
128
|
+
"@types/node": "^26.1.2"
|
|
129
|
+
},
|
|
130
|
+
"peerDependenciesMeta": {
|
|
131
|
+
"@types/node": {
|
|
132
|
+
"optional": true
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"devDependencies": {
|
|
136
|
+
"@elysia/cors": "^1.4.2",
|
|
137
|
+
"@napi-rs/cli": "^3.8.6",
|
|
138
|
+
"@types/bun": "^1.3.14",
|
|
139
|
+
"@types/mime-types": "^3.0.1",
|
|
140
|
+
"autocannon": "^8.0.0",
|
|
141
|
+
"@types/validator": "^13.15.10",
|
|
142
|
+
"ajv": "^8.20.0",
|
|
143
|
+
"crc-32": "^1.2.2",
|
|
144
|
+
"dts-bundle-generator": "^9.5.1",
|
|
145
|
+
"elysia": "^1.4.29",
|
|
146
|
+
"fast-json-patch": "^3.1.1",
|
|
147
|
+
"mime-types": "^3.0.2",
|
|
148
|
+
"typescript": "7.0.2",
|
|
149
|
+
"validator": "^13.15.35"
|
|
150
|
+
},
|
|
151
|
+
"overrides": {
|
|
152
|
+
"typescript": "7.0.2",
|
|
153
|
+
"dts-bundle-generator": {
|
|
154
|
+
"typescript": "5.9.3"
|
|
151
155
|
},
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
"node": ">=20.3"
|
|
156
|
+
"@napi-rs/cli": {
|
|
157
|
+
"typescript": "6.0.3"
|
|
155
158
|
},
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
159
|
+
"hyperid": "^4.0.0"
|
|
160
|
+
},
|
|
161
|
+
"engines": {
|
|
162
|
+
"bun": ">=1.1.0",
|
|
163
|
+
"node": ">=20.3"
|
|
164
|
+
},
|
|
165
|
+
"napi": {
|
|
166
|
+
"binaryName": "castrum",
|
|
167
|
+
"targets": [
|
|
168
|
+
"aarch64-apple-darwin",
|
|
169
|
+
"aarch64-unknown-linux-gnu",
|
|
170
|
+
"aarch64-unknown-linux-musl",
|
|
171
|
+
"x86_64-apple-darwin",
|
|
172
|
+
"x86_64-pc-windows-msvc",
|
|
173
|
+
"x86_64-unknown-linux-gnu",
|
|
174
|
+
"x86_64-unknown-linux-musl"
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
}
|
package/rust/crypto/pbkdf2.rs
CHANGED
|
@@ -17,6 +17,15 @@ pub const PBKDF2_MIN_LEN: u32 = 1;
|
|
|
17
17
|
/// Cap derived-key length so a hostile caller can't request a huge allocation.
|
|
18
18
|
pub const PBKDF2_MAX_LEN: u32 = 1024 * 1024;
|
|
19
19
|
|
|
20
|
+
/// Pure core (no napi types) for [`pbkdf2_sha256`] and the off-thread task
|
|
21
|
+
/// runtime: derive `dk_len` bytes, clamping the length to the documented range.
|
|
22
|
+
pub fn pbkdf2_sha256_into(password: &[u8], salt: &[u8], rounds: u32, dk_len: u32) -> Vec<u8> {
|
|
23
|
+
let dk_len = dk_len.clamp(PBKDF2_MIN_LEN, PBKDF2_MAX_LEN) as usize;
|
|
24
|
+
let mut out = vec![0u8; dk_len];
|
|
25
|
+
pbkdf2_hmac::<Sha256>(password, salt, rounds.max(1), &mut out);
|
|
26
|
+
out
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/// Derive a PBKDF2-HMAC-SHA256 key: `out = PBKDF2(password, salt, rounds, dkLen)`.
|
|
21
30
|
#[napi]
|
|
22
31
|
pub fn pbkdf2_sha256(
|
package/rust/crypto/session.rs
CHANGED
|
@@ -83,13 +83,10 @@ pub fn open_core(token: &[u8], secret: &[u8]) -> Option<(i64, Vec<u8>, Vec<u8>)>
|
|
|
83
83
|
if bytes.get(pos) != Some(&b'"') {
|
|
84
84
|
return None;
|
|
85
85
|
}
|
|
86
|
-
let
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
id = Some(bytes[vs..ve].to_vec());
|
|
92
|
-
pos = ve + 1;
|
|
86
|
+
let body = pos + 1;
|
|
87
|
+
let end = scan_json_string_end(bytes, body)?;
|
|
88
|
+
id = Some(unescape_json_id(&bytes[body..end]));
|
|
89
|
+
pos = end + 1;
|
|
93
90
|
}
|
|
94
91
|
b"exp" => {
|
|
95
92
|
let vs = pos;
|
|
@@ -174,6 +171,108 @@ fn skip_json_value(bytes: &[u8], pos: usize) -> Option<usize> {
|
|
|
174
171
|
}
|
|
175
172
|
}
|
|
176
173
|
|
|
174
|
+
/// Index of the `"` that TERMINATES a JSON string whose body starts at `start`
|
|
175
|
+
/// (just past the opening quote).
|
|
176
|
+
///
|
|
177
|
+
/// Escape-aware: the id used to be captured by scanning for the next `"`, which
|
|
178
|
+
/// stops at the quote inside an escaped `\"` — truncating the id AND leaving
|
|
179
|
+
/// `pos` in the middle of the string, so the rest of the envelope was then read
|
|
180
|
+
/// as structure (a quote-carrying id could make `open` fail outright).
|
|
181
|
+
fn scan_json_string_end(bytes: &[u8], start: usize) -> Option<usize> {
|
|
182
|
+
let mut i = start;
|
|
183
|
+
while i < bytes.len() {
|
|
184
|
+
match bytes[i] {
|
|
185
|
+
b'\\' => i += 2,
|
|
186
|
+
b'"' => return Some(i),
|
|
187
|
+
_ => i += 1,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
None
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Decode 4 hex digits at `at` (the `\uXXXX` payload).
|
|
194
|
+
fn hex4(bytes: &[u8], at: usize) -> Option<u32> {
|
|
195
|
+
let mut v = 0u32;
|
|
196
|
+
for k in 0..4 {
|
|
197
|
+
v = (v << 4) | (*bytes.get(at + k)? as char).to_digit(16)?;
|
|
198
|
+
}
|
|
199
|
+
Some(v)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Inverse of `json_ser::write_json_escaped` for the id: decodes `\"`, `\\`,
|
|
203
|
+
/// `\/`, `\b`, `\f`, `\n`, `\r`, `\t` and `\uXXXX`.
|
|
204
|
+
///
|
|
205
|
+
/// `\u00XX` maps back to the single BYTE `XX`. That is deliberate and load
|
|
206
|
+
/// bearing: the escaper emits that form for control characters AND — in its
|
|
207
|
+
/// invalid-UTF-8 mode — for every byte of binary input, so routing it through a
|
|
208
|
+
/// code point and re-encoding as UTF-8 would expand bytes >= 0x80 into two
|
|
209
|
+
/// bytes and destroy the byte-exact round trip the id is supposed to have.
|
|
210
|
+
/// (A valid-UTF-8 id is written RAW by the escaper, so `\u00XX` cannot be its
|
|
211
|
+
/// origin.)
|
|
212
|
+
///
|
|
213
|
+
/// `\uXXXX` above 0xFF can only come from a different producer (e.g. JS
|
|
214
|
+
/// `JSON.stringify` of a non-ASCII id), so it is decoded as a code point with
|
|
215
|
+
/// surrogate-pair combining. Unknown escapes and a trailing backslash are kept
|
|
216
|
+
/// verbatim — never silently dropped.
|
|
217
|
+
fn unescape_json_id(escaped: &[u8]) -> Vec<u8> {
|
|
218
|
+
let mut out = Vec::with_capacity(escaped.len());
|
|
219
|
+
let mut i = 0usize;
|
|
220
|
+
while i < escaped.len() {
|
|
221
|
+
if escaped[i] != b'\\' {
|
|
222
|
+
out.push(escaped[i]);
|
|
223
|
+
i += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
let Some(&e) = escaped.get(i + 1) else {
|
|
227
|
+
out.push(b'\\');
|
|
228
|
+
break;
|
|
229
|
+
};
|
|
230
|
+
i += 2;
|
|
231
|
+
match e {
|
|
232
|
+
b'"' => out.push(b'"'),
|
|
233
|
+
b'\\' => out.push(b'\\'),
|
|
234
|
+
b'/' => out.push(b'/'),
|
|
235
|
+
b'b' => out.push(0x08),
|
|
236
|
+
b'f' => out.push(0x0c),
|
|
237
|
+
b'n' => out.push(b'\n'),
|
|
238
|
+
b'r' => out.push(b'\r'),
|
|
239
|
+
b't' => out.push(b'\t'),
|
|
240
|
+
b'u' => {
|
|
241
|
+
let Some(cp) = hex4(escaped, i) else {
|
|
242
|
+
out.push(b'\\');
|
|
243
|
+
out.push(b'u');
|
|
244
|
+
continue;
|
|
245
|
+
};
|
|
246
|
+
i += 4;
|
|
247
|
+
if cp <= 0xFF {
|
|
248
|
+
out.push(cp as u8);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
let mut code = cp;
|
|
252
|
+
if (0xD800..=0xDBFF).contains(&cp)
|
|
253
|
+
&& escaped.get(i) == Some(&b'\\')
|
|
254
|
+
&& escaped.get(i + 1) == Some(&b'u')
|
|
255
|
+
{
|
|
256
|
+
if let Some(lo) = hex4(escaped, i + 2) {
|
|
257
|
+
if (0xDC00..=0xDFFF).contains(&lo) {
|
|
258
|
+
code = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
|
|
259
|
+
i += 6;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let mut buf = [0u8; 4];
|
|
264
|
+
let ch = char::from_u32(code).unwrap_or('\u{FFFD}');
|
|
265
|
+
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
|
|
266
|
+
}
|
|
267
|
+
other => {
|
|
268
|
+
out.push(b'\\');
|
|
269
|
+
out.push(other);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
out
|
|
274
|
+
}
|
|
275
|
+
|
|
177
276
|
#[cfg(test)]
|
|
178
277
|
mod tests {
|
|
179
278
|
use super::*;
|
|
@@ -209,6 +308,64 @@ mod tests {
|
|
|
209
308
|
assert!(seal_core(b"", b"{}", 0, SECRET).is_none());
|
|
210
309
|
}
|
|
211
310
|
|
|
311
|
+
#[test]
|
|
312
|
+
fn id_round_trips_byte_exactly_through_escapes() {
|
|
313
|
+
// A `"` used to terminate the id scan early, and the id came back
|
|
314
|
+
// escaped. Both are the same root cause: an escape-unaware scan with no
|
|
315
|
+
// decode. Every id here must survive the envelope byte-for-byte.
|
|
316
|
+
for id in [
|
|
317
|
+
&b"plain-id"[..],
|
|
318
|
+
&b"sess\"quote"[..],
|
|
319
|
+
&b"back\\slash"[..],
|
|
320
|
+
&b"new\nline"[..],
|
|
321
|
+
&b"tab\there"[..],
|
|
322
|
+
&b"cr\rhere"[..],
|
|
323
|
+
&b"nul\0byte"[..],
|
|
324
|
+
&b"binary\xff\xfe"[..],
|
|
325
|
+
&b"trailing\\"[..],
|
|
326
|
+
] {
|
|
327
|
+
let token = seal_core(id, br#"{"a":1}"#, 42, SECRET).expect("seal");
|
|
328
|
+
let (exp, got, data) = open_core(&token, SECRET).expect("open");
|
|
329
|
+
assert_eq!(got, id, "id must round-trip byte-exactly");
|
|
330
|
+
assert_eq!(exp, 42);
|
|
331
|
+
assert_eq!(data, br#"{"a":1}"#);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn unescape_json_id_matches_the_escaper() {
|
|
337
|
+
assert_eq!(unescape_json_id(br#"a\"b"#), b"a\"b");
|
|
338
|
+
assert_eq!(unescape_json_id(br#"a\\b"#), b"a\\b");
|
|
339
|
+
assert_eq!(unescape_json_id(br#"a\/b"#), b"a/b");
|
|
340
|
+
assert_eq!(unescape_json_id(br#"a\nb"#), b"a\nb");
|
|
341
|
+
assert_eq!(unescape_json_id(br#"a\rb"#), b"a\rb");
|
|
342
|
+
assert_eq!(unescape_json_id(br#"a\tb"#), b"a\tb");
|
|
343
|
+
assert_eq!(unescape_json_id(br#"a\bb"#), b"a\x08b");
|
|
344
|
+
assert_eq!(unescape_json_id(br#"a\fb"#), b"a\x0cb");
|
|
345
|
+
// `\u00XX` is a BYTE (the escaper's binary mode), not a code point.
|
|
346
|
+
assert_eq!(unescape_json_id(br#"a\u00e9b"#), b"a\xe9b");
|
|
347
|
+
// Above 0xFF is a real code point → UTF-8.
|
|
348
|
+
assert_eq!(unescape_json_id(br#"a\u20acb"#), "a\u{20ac}b".as_bytes());
|
|
349
|
+
// A surrogate pair combines into one code point.
|
|
350
|
+
assert_eq!(unescape_json_id(br#"\ud83d\ude00"#), "\u{1f600}".as_bytes());
|
|
351
|
+
// Unknown escapes and a trailing backslash stay verbatim.
|
|
352
|
+
assert_eq!(unescape_json_id(br#"a\qb"#), br#"a\qb"#);
|
|
353
|
+
assert_eq!(unescape_json_id(b"a\\"), b"a\\");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#[test]
|
|
357
|
+
fn scan_json_string_end_skips_escaped_quotes() {
|
|
358
|
+
// `a\"b"` (a, \, ", b, ") — the escaped quote does NOT terminate; the
|
|
359
|
+
// plain quote at index 4 does.
|
|
360
|
+
assert_eq!(scan_json_string_end(b"a\\\"b\"", 0), Some(4));
|
|
361
|
+
// No escape at all: the first quote terminates.
|
|
362
|
+
assert_eq!(scan_json_string_end(b"ab\"cd", 0), Some(2));
|
|
363
|
+
// Unterminated: the trailing backslash swallows the rest.
|
|
364
|
+
assert_eq!(scan_json_string_end(b"ab\\\"", 0), None);
|
|
365
|
+
// Escaped BACKSLASH then a terminator: `a\\"` = a, \, \, " → Some(3).
|
|
366
|
+
assert_eq!(scan_json_string_end(b"a\\\\\"", 0), Some(3));
|
|
367
|
+
}
|
|
368
|
+
|
|
212
369
|
#[test]
|
|
213
370
|
fn wire_format_matches_sign_cookie() {
|
|
214
371
|
// The sealed token must be verifiable by the EXISTING cookie verify
|