nyxa-speedups 0.1.1b0__tar.gz
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.
Potentially problematic release.
This version of nyxa-speedups might be problematic. Click here for more details.
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "heck"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "libc"
|
|
13
|
+
version = "0.2.190"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "nyxa-speedups"
|
|
19
|
+
version = "0.1.0"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pyo3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[[package]]
|
|
25
|
+
name = "once_cell"
|
|
26
|
+
version = "1.21.4"
|
|
27
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
28
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
29
|
+
|
|
30
|
+
[[package]]
|
|
31
|
+
name = "portable-atomic"
|
|
32
|
+
version = "1.15.0"
|
|
33
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
34
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
35
|
+
|
|
36
|
+
[[package]]
|
|
37
|
+
name = "proc-macro2"
|
|
38
|
+
version = "1.0.107"
|
|
39
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
40
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
41
|
+
dependencies = [
|
|
42
|
+
"unicode-ident",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[[package]]
|
|
46
|
+
name = "pyo3"
|
|
47
|
+
version = "0.29.3"
|
|
48
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
49
|
+
checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
|
|
50
|
+
dependencies = [
|
|
51
|
+
"libc",
|
|
52
|
+
"once_cell",
|
|
53
|
+
"portable-atomic",
|
|
54
|
+
"pyo3-build-config",
|
|
55
|
+
"pyo3-ffi",
|
|
56
|
+
"pyo3-macros",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "pyo3-build-config"
|
|
61
|
+
version = "0.29.3"
|
|
62
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
63
|
+
checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
|
|
64
|
+
dependencies = [
|
|
65
|
+
"target-lexicon",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[[package]]
|
|
69
|
+
name = "pyo3-ffi"
|
|
70
|
+
version = "0.29.3"
|
|
71
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
72
|
+
checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
|
|
73
|
+
dependencies = [
|
|
74
|
+
"libc",
|
|
75
|
+
"pyo3-build-config",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[[package]]
|
|
79
|
+
name = "pyo3-macros"
|
|
80
|
+
version = "0.29.3"
|
|
81
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
82
|
+
checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
|
|
83
|
+
dependencies = [
|
|
84
|
+
"proc-macro2",
|
|
85
|
+
"pyo3-macros-backend",
|
|
86
|
+
"quote",
|
|
87
|
+
"syn",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[[package]]
|
|
91
|
+
name = "pyo3-macros-backend"
|
|
92
|
+
version = "0.29.3"
|
|
93
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
94
|
+
checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
|
|
95
|
+
dependencies = [
|
|
96
|
+
"heck",
|
|
97
|
+
"proc-macro2",
|
|
98
|
+
"quote",
|
|
99
|
+
"syn",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[[package]]
|
|
103
|
+
name = "quote"
|
|
104
|
+
version = "1.0.47"
|
|
105
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
106
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
107
|
+
dependencies = [
|
|
108
|
+
"proc-macro2",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[[package]]
|
|
112
|
+
name = "syn"
|
|
113
|
+
version = "2.0.119"
|
|
114
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
115
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
116
|
+
dependencies = [
|
|
117
|
+
"proc-macro2",
|
|
118
|
+
"quote",
|
|
119
|
+
"unicode-ident",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[[package]]
|
|
123
|
+
name = "target-lexicon"
|
|
124
|
+
version = "0.13.5"
|
|
125
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
126
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
127
|
+
|
|
128
|
+
[[package]]
|
|
129
|
+
name = "unicode-ident"
|
|
130
|
+
version = "1.0.26"
|
|
131
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
132
|
+
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "nyxa-speedups"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Optional native accelerators for the Nyxa framework"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "nyxa_speedups"
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
pyo3 = { version = "0.29", features = ["extension-module", "abi3-py313"] }
|
|
15
|
+
|
|
16
|
+
[profile.release]
|
|
17
|
+
lto = "fat"
|
|
18
|
+
codegen-units = 1
|
|
19
|
+
opt-level = 3
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nyxa-speedups
|
|
3
|
+
Version: 0.1.1b0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Summary: Optional native accelerators for the Nyxa framework (install via nyxa[speed])
|
|
7
|
+
Author-email: Al-Amin Islam Nerob <alamin@aincoder.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.13
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.5,<2.0"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nyxa-speedups"
|
|
7
|
+
version = "0.1.1-beta"
|
|
8
|
+
description = "Optional native accelerators for the Nyxa framework (install via nyxa[speed])"
|
|
9
|
+
authors = [{ name = "Al-Amin Islam Nerob", email = "alamin@aincoder.com" }]
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
classifiers = ["Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython"]
|
|
13
|
+
|
|
14
|
+
[tool.maturin]
|
|
15
|
+
module-name = "nyxa_speedups"
|
|
16
|
+
|
|
17
|
+
# Rebuild in the parent project's env when the Rust sources change
|
|
18
|
+
[tool.uv]
|
|
19
|
+
cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "src/**/*.rs" }]
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
//! Native accelerators for Nyxa's request path (`nyxa[speed]`).
|
|
2
|
+
//!
|
|
3
|
+
//! Pure data work only. Every function mirrors a Python reference in
|
|
4
|
+
//! `nyxa._speedups`; inputs outside the fast path return `None` so the caller
|
|
5
|
+
//! falls back to Python, keeping results identical.
|
|
6
|
+
|
|
7
|
+
use std::borrow::Cow;
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
|
|
10
|
+
use pyo3::prelude::*;
|
|
11
|
+
use pyo3::types::{PyBytes, PyDict, PyList, PyString};
|
|
12
|
+
|
|
13
|
+
// -- path matcher ----------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
struct Edge {
|
|
16
|
+
name: Py<PyString>,
|
|
17
|
+
pattern: Option<Py<PyAny>>,
|
|
18
|
+
child: usize,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[derive(Default)]
|
|
22
|
+
struct Node {
|
|
23
|
+
statics: HashMap<String, usize>,
|
|
24
|
+
params: Vec<Edge>,
|
|
25
|
+
wildcard: Option<usize>,
|
|
26
|
+
handlers: Vec<(Vec<String>, Py<PyAny>)>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Mirror of `nyxa.routing.tree.RouteTree` built node by node from Python.
|
|
30
|
+
///
|
|
31
|
+
/// `find` returns the same route and params as the Python walk: static edge,
|
|
32
|
+
/// then param edges in order (constraints call the compiled Python pattern),
|
|
33
|
+
/// then the wildcard; at a terminal node the first handler serving the
|
|
34
|
+
/// method wins (`HEAD` falls back to `GET`).
|
|
35
|
+
#[pyclass(module = "nyxa_speedups", frozen)]
|
|
36
|
+
struct Matcher {
|
|
37
|
+
nodes: std::sync::RwLock<Vec<Node>>,
|
|
38
|
+
wildcard: Py<PyString>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn accepts(methods: &[String], method: &str) -> bool {
|
|
42
|
+
methods.iter().any(|m| m == method) || (method == "HEAD" && methods.iter().any(|m| m == "GET"))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
struct Search<'n, 'p> {
|
|
46
|
+
nodes: &'n [Node],
|
|
47
|
+
segs: Vec<&'p str>,
|
|
48
|
+
method: &'p str,
|
|
49
|
+
caps: Vec<(usize, usize, &'p str)>,
|
|
50
|
+
wildcard: Option<String>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl Search<'_, '_> {
|
|
54
|
+
/// `(node, handler index)` of the first route serving the method below `node`.
|
|
55
|
+
fn find(&mut self, py: Python<'_>, node: usize, i: usize) -> PyResult<Option<(usize, usize)>> {
|
|
56
|
+
let nodes = self.nodes;
|
|
57
|
+
let n = &nodes[node];
|
|
58
|
+
if i == self.segs.len() {
|
|
59
|
+
let method = self.method;
|
|
60
|
+
return Ok(n
|
|
61
|
+
.handlers
|
|
62
|
+
.iter()
|
|
63
|
+
.position(|(methods, _)| accepts(methods, method))
|
|
64
|
+
.map(|k| (node, k)));
|
|
65
|
+
}
|
|
66
|
+
let seg = self.segs[i];
|
|
67
|
+
if let Some(&child) = n.statics.get(seg) {
|
|
68
|
+
if let Some(hit) = self.find(py, child, i + 1)? {
|
|
69
|
+
return Ok(Some(hit));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (k, edge) in n.params.iter().enumerate() {
|
|
73
|
+
let ok = match &edge.pattern {
|
|
74
|
+
None => true,
|
|
75
|
+
Some(p) => !p.bind(py).call_method1("fullmatch", (seg,))?.is_none(),
|
|
76
|
+
};
|
|
77
|
+
if ok {
|
|
78
|
+
self.caps.push((node, k, seg));
|
|
79
|
+
if let Some(hit) = self.find(py, edge.child, i + 1)? {
|
|
80
|
+
return Ok(Some(hit));
|
|
81
|
+
}
|
|
82
|
+
self.caps.pop();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if let Some(child) = n.wildcard {
|
|
86
|
+
let len = self.segs.len();
|
|
87
|
+
if let Some(hit) = self.find(py, child, len)? {
|
|
88
|
+
self.wildcard = Some(self.segs[i..].join("/"));
|
|
89
|
+
return Ok(Some(hit));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
Ok(None)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[pymethods]
|
|
97
|
+
impl Matcher {
|
|
98
|
+
#[new]
|
|
99
|
+
fn new(py: Python<'_>) -> Self {
|
|
100
|
+
Self {
|
|
101
|
+
nodes: std::sync::RwLock::new(vec![Node::default()]),
|
|
102
|
+
wildcard: PyString::intern(py, "wildcard").unbind(),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Create an empty node; returns its id (the root is 0).
|
|
107
|
+
fn add_node(&self) -> usize {
|
|
108
|
+
let mut nodes = self.nodes.write().unwrap();
|
|
109
|
+
nodes.push(Node::default());
|
|
110
|
+
nodes.len() - 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn add_static(&self, parent: usize, segment: String, child: usize) {
|
|
114
|
+
self.nodes.write().unwrap()[parent]
|
|
115
|
+
.statics
|
|
116
|
+
.insert(segment, child);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Param edge; `pattern` is a compiled Python regex (``fullmatch``) or ``None``.
|
|
120
|
+
#[pyo3(signature = (parent, name, pattern, child))]
|
|
121
|
+
fn add_param(
|
|
122
|
+
&self,
|
|
123
|
+
parent: usize,
|
|
124
|
+
name: Py<PyString>,
|
|
125
|
+
pattern: Option<Py<PyAny>>,
|
|
126
|
+
child: usize,
|
|
127
|
+
) {
|
|
128
|
+
self.nodes.write().unwrap()[parent].params.push(Edge {
|
|
129
|
+
name,
|
|
130
|
+
pattern,
|
|
131
|
+
child,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn set_wildcard(&self, parent: usize, child: usize) {
|
|
136
|
+
self.nodes.write().unwrap()[parent].wildcard = Some(child);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Append a terminal handler (walk order) serving `methods`.
|
|
140
|
+
fn add_handler(&self, node: usize, methods: Vec<String>, route: Py<PyAny>) {
|
|
141
|
+
self.nodes.write().unwrap()[node]
|
|
142
|
+
.handlers
|
|
143
|
+
.push((methods, route));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// ``(route, params)`` for the first route serving ``method``, else ``None``.
|
|
147
|
+
fn find<'py>(
|
|
148
|
+
&self,
|
|
149
|
+
py: Python<'py>,
|
|
150
|
+
method: &str,
|
|
151
|
+
path: &str,
|
|
152
|
+
) -> PyResult<Option<(Bound<'py, PyAny>, Bound<'py, PyDict>)>> {
|
|
153
|
+
let nodes = self.nodes.read().unwrap();
|
|
154
|
+
let trimmed = path.trim_matches('/');
|
|
155
|
+
let segs = if trimmed.is_empty() {
|
|
156
|
+
Vec::new()
|
|
157
|
+
} else {
|
|
158
|
+
trimmed.split('/').collect()
|
|
159
|
+
};
|
|
160
|
+
let mut search = Search {
|
|
161
|
+
nodes: &nodes,
|
|
162
|
+
segs,
|
|
163
|
+
method,
|
|
164
|
+
caps: Vec::with_capacity(4),
|
|
165
|
+
wildcard: None,
|
|
166
|
+
};
|
|
167
|
+
let Some((node, k)) = search.find(py, 0, 0)? else {
|
|
168
|
+
return Ok(None);
|
|
169
|
+
};
|
|
170
|
+
let route = nodes[node].handlers[k].1.bind(py).clone();
|
|
171
|
+
let params = PyDict::new(py);
|
|
172
|
+
for (node, k, value) in &search.caps {
|
|
173
|
+
params.set_item(nodes[*node].params[*k].name.bind(py), *value)?;
|
|
174
|
+
}
|
|
175
|
+
if let Some(joined) = search.wildcard {
|
|
176
|
+
params.set_item(self.wildcard.bind(py), joined)?;
|
|
177
|
+
}
|
|
178
|
+
Ok(Some((route, params)))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// -- query string ------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
fn hex(b: u8) -> Option<u8> {
|
|
185
|
+
match b {
|
|
186
|
+
b'0'..=b'9' => Some(b - b'0'),
|
|
187
|
+
b'a'..=b'f' => Some(b - b'a' + 10),
|
|
188
|
+
b'A'..=b'F' => Some(b - b'A' + 10),
|
|
189
|
+
_ => None,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// `parse_qsl` unquoting (`+` → space, `%XX`); `None` when the result isn't UTF-8.
|
|
194
|
+
fn unquote_plus(raw: &str) -> Option<Cow<'_, str>> {
|
|
195
|
+
if !raw.bytes().any(|b| b == b'%' || b == b'+') {
|
|
196
|
+
return Some(Cow::Borrowed(raw));
|
|
197
|
+
}
|
|
198
|
+
let bytes = raw.as_bytes();
|
|
199
|
+
let mut out = Vec::with_capacity(bytes.len());
|
|
200
|
+
let mut i = 0;
|
|
201
|
+
while i < bytes.len() {
|
|
202
|
+
let b = bytes[i];
|
|
203
|
+
if b == b'+' {
|
|
204
|
+
out.push(b' ');
|
|
205
|
+
} else if b == b'%' && i + 2 < bytes.len() {
|
|
206
|
+
if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
|
|
207
|
+
out.push(h * 16 + l);
|
|
208
|
+
i += 3;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
out.push(b);
|
|
212
|
+
} else {
|
|
213
|
+
out.push(b);
|
|
214
|
+
}
|
|
215
|
+
i += 1;
|
|
216
|
+
}
|
|
217
|
+
String::from_utf8(out).ok().map(Cow::Owned)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// `dict(parse_qsl(qs, keep_blank_values=True))`, or `None` to fall back.
|
|
221
|
+
#[pyfunction]
|
|
222
|
+
fn parse_query<'py>(py: Python<'py>, qs: &str) -> PyResult<Option<Bound<'py, PyDict>>> {
|
|
223
|
+
let out = PyDict::new(py);
|
|
224
|
+
for piece in qs.split('&') {
|
|
225
|
+
if piece.is_empty() {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
let (k, v) = piece.split_once('=').unwrap_or((piece, ""));
|
|
229
|
+
let (Some(k), Some(v)) = (unquote_plus(k), unquote_plus(v)) else {
|
|
230
|
+
return Ok(None);
|
|
231
|
+
};
|
|
232
|
+
out.set_item(k.as_ref(), v.as_ref())?;
|
|
233
|
+
}
|
|
234
|
+
Ok(Some(out))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// -- cookies ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
fn cookie_name_char(b: u8) -> bool {
|
|
240
|
+
b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~:".contains(&b)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn cookie_value_char(b: u8) -> bool {
|
|
244
|
+
b.is_ascii_alphanumeric() || b"!#%&'~_`><@,:/$*+-.^|)(?}{=".contains(&b)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const RESERVED: [&str; 9] = [
|
|
248
|
+
"expires", "path", "comment", "domain", "max-age", "secure", "httponly", "version", "samesite",
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
/// Plain `name=value; ...` cookie headers as `SimpleCookie` reads them; `None`
|
|
252
|
+
/// for anything else (quotes, attributes, `$` keys, non-ASCII, stray `;`).
|
|
253
|
+
#[pyfunction]
|
|
254
|
+
fn parse_cookies<'py>(py: Python<'py>, header: &str) -> PyResult<Option<Bound<'py, PyDict>>> {
|
|
255
|
+
let out = PyDict::new(py);
|
|
256
|
+
let mut pieces = header.split(';').peekable();
|
|
257
|
+
while let Some(piece) = pieces.next() {
|
|
258
|
+
let piece = piece.trim_matches(|c: char| c == ' ' || c == '\t');
|
|
259
|
+
if piece.is_empty() {
|
|
260
|
+
// SimpleCookie stops at an empty segment; only a trailing one is harmless
|
|
261
|
+
if pieces.peek().is_some() {
|
|
262
|
+
return Ok(None);
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
let Some((name, value)) = piece.split_once('=') else {
|
|
267
|
+
return Ok(None);
|
|
268
|
+
};
|
|
269
|
+
let (name, value) = (name.trim_end(), value.trim_start());
|
|
270
|
+
if name.is_empty()
|
|
271
|
+
|| name.starts_with('$')
|
|
272
|
+
|| !name.bytes().all(cookie_name_char)
|
|
273
|
+
|| !value.bytes().all(cookie_value_char)
|
|
274
|
+
|| RESERVED.iter().any(|r| name.eq_ignore_ascii_case(r))
|
|
275
|
+
{
|
|
276
|
+
return Ok(None);
|
|
277
|
+
}
|
|
278
|
+
out.set_item(name, value)?;
|
|
279
|
+
}
|
|
280
|
+
Ok(Some(out))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// -- ASGI headers ------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/// `(x-request-id value | None, declares_body)` in one pass over ASGI request
|
|
286
|
+
/// headers; `None` when an item isn't a `(bytes, value)` pair.
|
|
287
|
+
#[pyfunction]
|
|
288
|
+
fn scan_headers<'py>(
|
|
289
|
+
headers: &Bound<'py, PyAny>,
|
|
290
|
+
) -> PyResult<Option<(Option<Bound<'py, PyAny>>, bool)>> {
|
|
291
|
+
let mut rid = None;
|
|
292
|
+
let mut declared = false;
|
|
293
|
+
for item in headers.try_iter()? {
|
|
294
|
+
let item = item?;
|
|
295
|
+
let Ok(name) = item.get_item(0) else {
|
|
296
|
+
return Ok(None);
|
|
297
|
+
};
|
|
298
|
+
let Ok(name) = name.cast::<PyBytes>() else {
|
|
299
|
+
return Ok(None);
|
|
300
|
+
};
|
|
301
|
+
let name = name.as_bytes();
|
|
302
|
+
if name == b"x-request-id" {
|
|
303
|
+
rid = Some(item.get_item(1)?);
|
|
304
|
+
} else if name == b"content-length" || name == b"transfer-encoding" {
|
|
305
|
+
declared = true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
Ok(Some((rid, declared)))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// ASGI response header list for an all-ASCII `dict[str, str]`; `None` to fall back.
|
|
312
|
+
#[pyfunction]
|
|
313
|
+
fn encode_headers<'py>(
|
|
314
|
+
py: Python<'py>,
|
|
315
|
+
headers: &Bound<'py, PyDict>,
|
|
316
|
+
) -> PyResult<Option<Bound<'py, PyList>>> {
|
|
317
|
+
let out = PyList::empty(py);
|
|
318
|
+
for (k, v) in headers.iter() {
|
|
319
|
+
let (Ok(k), Ok(v)) = (k.cast::<PyString>(), v.cast::<PyString>()) else {
|
|
320
|
+
return Ok(None);
|
|
321
|
+
};
|
|
322
|
+
let (k, v) = (k.to_str()?, v.to_str()?);
|
|
323
|
+
if !k.is_ascii() || !v.is_ascii() {
|
|
324
|
+
return Ok(None);
|
|
325
|
+
}
|
|
326
|
+
out.append((
|
|
327
|
+
PyBytes::new(py, k.as_bytes()),
|
|
328
|
+
PyBytes::new(py, v.as_bytes()),
|
|
329
|
+
))?;
|
|
330
|
+
}
|
|
331
|
+
Ok(Some(out))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[pymodule]
|
|
335
|
+
fn nyxa_speedups(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
336
|
+
m.add_class::<Matcher>()?;
|
|
337
|
+
m.add_function(wrap_pyfunction!(parse_query, m)?)?;
|
|
338
|
+
m.add_function(wrap_pyfunction!(parse_cookies, m)?)?;
|
|
339
|
+
m.add_function(wrap_pyfunction!(scan_headers, m)?)?;
|
|
340
|
+
m.add_function(wrap_pyfunction!(encode_headers, m)?)?;
|
|
341
|
+
Ok(())
|
|
342
|
+
}
|