Skip to content

Commit 465f0de

Browse files
committed
fix(headers): correctly handle overset after deletion
1 parent 26f8016 commit 465f0de

11 files changed

Lines changed: 581 additions & 26 deletions

examples/Cargo.toml

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
11
[workspace]
22
resolver = "3"
3-
members = [
4-
"sse",
5-
"jwt",
6-
"form",
7-
"hello",
8-
"chatgpt",
9-
"websocket",
10-
"basic_auth",
11-
"quick_start",
12-
"static_files",
13-
"json_response",
14-
"derive_from_request",
15-
"multiple-single-threads",
16-
]
3+
members = ["*"]
4+
exclude = ["target"]
175

186
[workspace.dependencies]
197
# set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami`
208
ohkami = { path = "../ohkami", default-features = false, features = ["rt_tokio", "sse", "ws"] }
219
tokio = { version = "1", features = ["full"] }
2210
tracing = "0.1"
23-
tracing-subscriber = "0.3"
11+
tracing-subscriber = "0.3"

examples/hello/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
2-
name = "hello"
3-
version = "0.1.0"
4-
edition = "2024"
2+
name = "hello"
3+
version = "0.1.0"
4+
edition = "2024"
55

66
[dependencies]
77
ohkami = { workspace = true }

examples/html_layout/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "html_layout"
3+
version = "0.0.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
ohkami = { workspace = true }
8+
tokio = { workspace = true }
9+
uibeam = "0.2"

examples/html_layout/src/main.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use ohkami::prelude::*;
2+
use ohkami::serde::Deserialize;
3+
use ohkami::format::{Query, HTML};
4+
use uibeam::{UI, Beam};
5+
6+
struct Layout {
7+
title: String,
8+
children: UI,
9+
}
10+
impl Beam for Layout {
11+
fn render(self) -> UI {
12+
UI! {
13+
<html>
14+
<head>
15+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" />
16+
<title>{&*self.title}</title>
17+
</head>
18+
<body>
19+
{self.children}
20+
</body>
21+
</html>
22+
}
23+
}
24+
}
25+
impl Layout {
26+
fn fang_with_title(title: &str) -> impl FangAction {
27+
#[derive(Clone)]
28+
struct Fang {
29+
title: String,
30+
}
31+
32+
impl FangAction for Fang {
33+
async fn back(&self, res: &mut Response) {
34+
if res.headers.ContentType().is_some_and(|x| x.starts_with("text/html")) {
35+
let content = res.drop_content().into_bytes().unwrap();
36+
let content = std::str::from_utf8(&*content).unwrap();
37+
res.set_html(uibeam::shoot(UI! {
38+
<Layout title={self.title.clone()}>
39+
unsafe {content}
40+
</Layout>
41+
}));
42+
}
43+
}
44+
}
45+
46+
Fang {
47+
title: title.to_string(),
48+
}
49+
}
50+
}
51+
52+
struct Counter {
53+
initial_count: i32,
54+
}
55+
impl Beam for Counter {
56+
fn render(self) -> UI {
57+
UI! {
58+
<div>
59+
<h1 class="text-2xl font-bold">
60+
"count: "<span id="count">{self.initial_count}</span>
61+
</h1>
62+
<button
63+
id="increment"
64+
class="bg-blue-500 text-white px-4 py-2 rounded"
65+
>"+"</button>
66+
<button
67+
id="decrement"
68+
class="bg-red-500 text-white px-4 py-2 rounded"
69+
>"-"</button>
70+
71+
<script>r#"
72+
const count = document.getElementById('count');
73+
document.getElementById('increment').addEventListener('click', () => {
74+
count.innerText = parseInt(count.innerText) + 1;
75+
});
76+
document.getElementById('decrement').addEventListener('click', () => {
77+
count.innerText = parseInt(count.innerText) - 1;
78+
});
79+
"#</script>
80+
</div>
81+
}
82+
}
83+
}
84+
85+
#[derive(Deserialize)]
86+
struct CounterMeta {
87+
init: Option<i32>,
88+
}
89+
90+
async fn index(Query(q): Query<CounterMeta>) -> HTML<std::borrow::Cow<'static, str>> {
91+
let initial_count = q.init.unwrap_or(0);
92+
93+
HTML(uibeam::shoot(UI! {
94+
<Counter initial_count={initial_count} />
95+
}))
96+
}
97+
98+
#[tokio::main]
99+
async fn main() {
100+
Ohkami::new((
101+
Layout::fang_with_title("Counter Example"),
102+
"/".GET(index),
103+
)).howl("localhost:5000").await
104+
}

ohkami/src/header/map.rs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,35 @@ impl<const N: usize, Value> IndexMap<N, Value> {
3838

3939
#[inline(always)]
4040
pub(crate) unsafe fn delete(&mut self, index: usize) {
41-
unsafe {*self.index.get_unchecked_mut(index) = Self::NULL}
41+
*unsafe {self.index.get_unchecked_mut(index)} = Self::NULL;
4242
}
4343

4444
#[inline(always)]
4545
pub(crate) unsafe fn set(&mut self, index: usize, value: Value) {
46-
unsafe {*self.index.get_unchecked_mut(index) = self.values.len() as u8}
46+
*unsafe {self.index.get_unchecked_mut(index)} = self.values.len() as u8;
4747
self.values.push((index, value));
4848
}
4949

5050
#[inline(always)]
51-
pub(crate) fn iter(&self) -> impl Iterator<Item = &(usize, Value)> {
51+
pub(crate) fn iter(&self) -> impl Iterator<Item = (usize, &Value)> {
5252
self.values.iter()
53-
.filter(|(i, _)| *unsafe {self.index.get_unchecked(*i)} != Self::NULL)
53+
.enumerate()
54+
.filter_map(|(pos, (index, value))| (
55+
// `!= Self::NULL` can't correctly handle *over-set after delete*,
56+
// we MUST check the held index to be equal to the current position
57+
*unsafe {self.index.get_unchecked(*index)} == pos as u8
58+
).then_some((*index, value)))
5459
}
5560

5661
#[inline(always)]
5762
pub(crate) fn into_iter(self) -> impl Iterator<Item = (usize, Value)> {
5863
self.values.into_iter()
59-
.filter(move |(i, _)| *unsafe {self.index.get_unchecked(*i)} != Self::NULL)
64+
.enumerate()
65+
.filter_map(move |(pos, (index, value))| (
66+
// `!= Self::NULL` can't correctly handle *over-set after delete*,
67+
// we MUST check the held index to be equal to the current position
68+
*unsafe {self.index.get_unchecked(index)} == pos as u8
69+
).then_some((index, value)))
6070
}
6171
}
6272

@@ -80,3 +90,83 @@ const _: () = {
8090
}
8191
}
8292
};
93+
94+
#[cfg(test)]
95+
mod test {
96+
use super::*;
97+
98+
#[test]
99+
fn test_index_map_iter_simple() {
100+
let mut map = IndexMap::<8, &'static str>::new();
101+
102+
unsafe {map.set(0, "a")};
103+
unsafe {map.set(1, "b")};
104+
unsafe {map.set(2, "c")};
105+
unsafe {map.set(3, "d")};
106+
107+
assert_eq!(
108+
map.into_iter().collect::<Vec<_>>(),
109+
vec![(0, "a"), (1, "b"), (2, "c"), (3, "d")]
110+
);
111+
}
112+
113+
#[test]
114+
fn test_index_map_iter_with_delete() {
115+
let mut map = IndexMap::<8, &'static str>::new();
116+
117+
unsafe {map.set(0, "a")};
118+
unsafe {map.set(1, "b")};
119+
unsafe {map.set(2, "c")};
120+
unsafe {map.set(3, "d")};
121+
122+
unsafe {map.delete(1)};
123+
unsafe {map.delete(3)};
124+
125+
assert_eq!(
126+
map.into_iter().collect::<Vec<_>>(),
127+
vec![(0, "a"), (2, "c")]
128+
);
129+
}
130+
131+
#[test]
132+
fn test_index_map_iter_with_delete_and_other_set() {
133+
let mut map = IndexMap::<8, &'static str>::new();
134+
135+
unsafe {map.set(0, "a")};
136+
unsafe {map.set(1, "b")};
137+
unsafe {map.set(2, "c")};
138+
unsafe {map.set(3, "d")};
139+
140+
unsafe {map.delete(1)};
141+
unsafe {map.delete(3)};
142+
143+
unsafe {map.set(4, "e")};
144+
unsafe {map.set(5, "f")};
145+
146+
assert_eq!(
147+
map.into_iter().collect::<Vec<_>>(),
148+
vec![(0, "a"), (2, "c"), (4, "e"), (5, "f")]
149+
);
150+
}
151+
152+
#[test]
153+
fn test_index_map_iter_with_delete_and_overset() {
154+
let mut map = IndexMap::<8, &'static str>::new();
155+
156+
unsafe {map.set(0, "a")};
157+
unsafe {map.set(1, "b")};
158+
unsafe {map.set(2, "c")};
159+
unsafe {map.set(3, "d")};
160+
161+
unsafe {map.delete(1)};
162+
unsafe {map.delete(3)};
163+
164+
unsafe {map.set(1, "e")};
165+
unsafe {map.set(3, "f")};
166+
167+
assert_eq!(
168+
map.into_iter().collect::<Vec<_>>(),
169+
vec![(0, "a"), (2, "c"), (1, "e"), (3, "f")]
170+
);
171+
}
172+
}

ohkami/src/request/headers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ impl Headers {
278278
pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
279279
self.standard.iter()
280280
.map(|(i, v)| (
281-
unsafe {std::mem::transmute::<_, Header>(*i as u8).as_str()},
281+
unsafe {std::mem::transmute::<_, Header>(i as u8).as_str()},
282282
std::str::from_utf8(v).expect("Non UTF-8 header value")
283283
))
284284
.chain(self.custom.as_ref()

ohkami/src/response/headers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ impl Headers {
439439
pub(crate) fn iter_standard(&self) -> impl Iterator<Item = (&str, &str)> {
440440
self.standard.iter()
441441
.map(|(i, v)| (
442-
unsafe {std::mem::transmute::<_, Header>(*i as u8)}.as_str(),
442+
unsafe {std::mem::transmute::<_, Header>(i as u8)}.as_str(),
443443
&**v
444444
))
445445
}
@@ -481,7 +481,7 @@ impl Headers {
481481
pub(crate) unsafe fn write_unchecked_to(&self, buf: &mut Vec<u8>) {
482482
unsafe {
483483
for (i, v) in self.standard.iter() {
484-
let h = std::mem::transmute::<_, Header>(*i as u8); {
484+
let h = std::mem::transmute::<_, Header>(i as u8); {
485485
crate::push_unchecked!(buf <- h.as_bytes());
486486
crate::push_unchecked!(buf <- b": ");
487487
crate::push_unchecked!(buf <- v.as_bytes());

0 commit comments

Comments
 (0)