1use crate::engine::sequence::{MessageHandle, StreamKey};
10use crate::model::{FRAGMENT_BODY_MAX, FRAGMENT_COUNT_MAX, MessageType, Regarding};
11
12pub const PAGE_SIZE: usize = 80;
13const PAGE_NONE: u8 = 0xFF;
14
15#[derive(Clone, Copy, Debug, Default)]
23pub struct FirstMeta {
24 pub message_type_byte: u8,
25 pub regarding: Option<Regarding>,
26 pub editing: Option<u8>,
27}
28
29impl FirstMeta {
30 pub fn message_type(&self) -> MessageType {
31 MessageType::from_byte(self.message_type_byte)
32 }
33}
34
35#[derive(Clone, Debug)]
37pub struct Slot {
38 pub stream: StreamKey,
39 pub epoch: u16,
40 pub message_id: u8,
41 pub count: u8,
42 pub present: u16,
44 pub unavailable: u16,
46 pub repair_exhausted: u16,
50 pub frag_len: [u8; FRAGMENT_COUNT_MAX as usize],
51 frag_head: [u8; FRAGMENT_COUNT_MAX as usize],
52 pub meta: FirstMeta,
53 pub have_meta: bool,
54 pub announced: bool,
56 pub late: bool,
59 pub notified: bool,
62 pub handle: MessageHandle,
63 pub created_ms: u64,
64 pub deadline_ms: u64,
65 pub repair_at_ms: u64,
67 pub last_fragment_ms: u64,
69}
70
71impl Slot {
72 pub fn is_complete(&self) -> bool {
73 let all = (1u16 << self.count) - 1;
74 self.present == all
75 }
76
77 pub fn is_settled(&self) -> bool {
80 let all = (1u16 << self.count) - 1;
81 (self.present | self.unavailable) == all
82 }
83
84 pub fn missing(&self) -> impl Iterator<Item = u8> + '_ {
85 (0..self.count).filter(|index| {
86 let bit = 1u16 << index;
87 self.present & bit == 0 && self.unavailable & bit == 0
88 })
89 }
90
91 pub fn repairable_missing(&self) -> impl Iterator<Item = u8> + '_ {
92 self.missing().filter(|index| {
93 let bit = 1u16 << index;
94 self.repair_exhausted & bit == 0
95 })
96 }
97
98 fn fragment_len(&self, index: u8) -> usize {
99 self.frag_len[index as usize] as usize
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum InsertOutcome {
106 Stored,
108 Duplicate,
110 Conflict,
113 NoSpace,
115 TooLarge,
117}
118
119pub struct ReassemblyPool<const SLOTS: usize, const PAGES: usize> {
121 pages: [[u8; PAGE_SIZE]; PAGES],
122 next_page: [u8; PAGES],
123 free_head: u8,
124 pub slots: [Option<Slot>; SLOTS],
125}
126
127impl<const SLOTS: usize, const PAGES: usize> ReassemblyPool<SLOTS, PAGES> {
128 pub fn new() -> Self {
129 assert!(PAGES < PAGE_NONE as usize, "page index must fit in u8");
130 let mut next_page = [PAGE_NONE; PAGES];
131 for (index, next) in next_page
132 .iter_mut()
133 .enumerate()
134 .take(PAGES.saturating_sub(1))
135 {
136 *next = (index + 1) as u8;
137 }
138 Self {
139 pages: [[0; PAGE_SIZE]; PAGES],
140 next_page,
141 free_head: if PAGES == 0 { PAGE_NONE } else { 0 },
142 slots: [const { None }; SLOTS],
143 }
144 }
145
146 fn free_pages(&self) -> usize {
147 let mut count = 0;
148 let mut cursor = self.free_head;
149 while cursor != PAGE_NONE {
150 count += 1;
151 cursor = self.next_page[cursor as usize];
152 }
153 count
154 }
155
156 fn alloc_chain(&mut self, len: usize) -> Option<u8> {
157 let needed = len.div_ceil(PAGE_SIZE).max(1);
158 if self.free_pages() < needed {
159 return None;
160 }
161 let head = self.free_head;
162 let mut cursor = head;
163 for _ in 1..needed {
164 cursor = self.next_page[cursor as usize];
165 }
166 self.free_head = self.next_page[cursor as usize];
167 self.next_page[cursor as usize] = PAGE_NONE;
168 Some(head)
169 }
170
171 fn free_chain(&mut self, head: u8) {
172 if head == PAGE_NONE {
173 return;
174 }
175 let mut cursor = head;
176 while self.next_page[cursor as usize] != PAGE_NONE {
177 cursor = self.next_page[cursor as usize];
178 }
179 self.next_page[cursor as usize] = self.free_head;
180 self.free_head = head;
181 }
182
183 fn write_chain(&mut self, head: u8, bytes: &[u8]) {
184 let mut cursor = head;
185 for chunk in bytes.chunks(PAGE_SIZE) {
186 self.pages[cursor as usize][..chunk.len()].copy_from_slice(chunk);
187 cursor = self.next_page[cursor as usize];
188 }
189 }
190
191 fn read_chain(&self, head: u8, len: usize, out: &mut [u8]) {
192 let mut cursor = head;
193 let mut offset = 0;
194 while offset < len {
195 let take = (len - offset).min(PAGE_SIZE);
196 out[offset..offset + take].copy_from_slice(&self.pages[cursor as usize][..take]);
197 offset += take;
198 cursor = self.next_page[cursor as usize];
199 }
200 }
201
202 pub fn read_fragment(&self, slot_index: usize, fragment: u8, out: &mut [u8]) -> usize {
205 let slot = self.slots[slot_index].as_ref().expect("occupied slot");
206 let len = slot.fragment_len(fragment);
207 self.read_chain(slot.frag_head[fragment as usize], len, out);
208 len
209 }
210
211 pub fn find_slot(&self, stream: &StreamKey, epoch: u16, message_id: u8) -> Option<usize> {
212 self.slots.iter().position(|slot| {
213 slot.as_ref().is_some_and(|slot| {
214 slot.stream == *stream && slot.epoch == epoch && slot.message_id == message_id
215 })
216 })
217 }
218
219 pub fn open_slot(&mut self, slot: Slot) -> Option<usize> {
220 let index = self.slots.iter().position(Option::is_none)?;
221 self.slots[index] = Some(slot);
222 Some(index)
223 }
224
225 pub fn oldest_slot(&self) -> Option<usize> {
227 self.slots
228 .iter()
229 .enumerate()
230 .filter_map(|(index, slot)| slot.as_ref().map(|slot| (index, slot.created_ms)))
231 .min_by_key(|(_, created)| *created)
232 .map(|(index, _)| index)
233 }
234
235 pub fn close_slot(&mut self, index: usize) -> Option<Slot> {
237 let slot = self.slots[index].take()?;
238 for head in slot.frag_head {
239 self.free_chain(head);
240 }
241 Some(slot)
242 }
243
244 pub fn drop_stream(&mut self, stream: &StreamKey) -> usize {
247 let mut dropped = 0;
248 for index in 0..SLOTS {
249 if self.slots[index]
250 .as_ref()
251 .is_some_and(|slot| slot.stream == *stream)
252 {
253 self.close_slot(index);
254 dropped += 1;
255 }
256 }
257 dropped
258 }
259
260 pub fn insert_fragment(&mut self, index: usize, fragment: u8, bytes: &[u8]) -> InsertOutcome {
262 if bytes.len() > FRAGMENT_BODY_MAX {
265 return InsertOutcome::TooLarge;
266 }
267 let slot = self.slots[index].as_ref().expect("occupied slot");
268 let bit = 1u16 << fragment;
269 if slot.present & bit != 0 {
270 let mut existing = [0u8; FRAGMENT_BODY_MAX];
271 let len = self.read_fragment(index, fragment, &mut existing);
272 return if &existing[..len] == bytes {
273 InsertOutcome::Duplicate
274 } else {
275 InsertOutcome::Conflict
276 };
277 }
278 let Some(head) = self.alloc_chain(bytes.len()) else {
279 return InsertOutcome::NoSpace;
280 };
281 self.write_chain(head, bytes);
282 let slot = self.slots[index].as_mut().expect("occupied slot");
283 slot.frag_head[fragment as usize] = head;
284 slot.frag_len[fragment as usize] = bytes.len() as u8;
285 slot.present |= bit;
286 slot.unavailable &= !bit;
287 InsertOutcome::Stored
288 }
289}
290
291impl<const SLOTS: usize, const PAGES: usize> Default for ReassemblyPool<SLOTS, PAGES> {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297pub fn empty_slot(
298 stream: StreamKey,
299 epoch: u16,
300 message_id: u8,
301 count: u8,
302 handle: MessageHandle,
303 now_ms: u64,
304) -> Slot {
305 Slot {
306 stream,
307 epoch,
308 message_id,
309 count,
310 present: 0,
311 unavailable: 0,
312 repair_exhausted: 0,
313 frag_len: [0; FRAGMENT_COUNT_MAX as usize],
314 frag_head: [PAGE_NONE; FRAGMENT_COUNT_MAX as usize],
315 meta: FirstMeta::default(),
316 have_meta: false,
317 announced: false,
318 late: false,
319 notified: false,
320 handle,
321 created_ms: now_ms,
322 deadline_ms: now_ms,
323 repair_at_ms: now_ms,
324 last_fragment_ms: now_ms,
325 }
326}
327
328struct LossyWriter<'a> {
331 out: &'a mut [u8],
332 pos: usize,
333 carry: [u8; 4],
334 carry_len: usize,
335 truncated: bool,
336 had_invalid: bool,
337}
338
339impl<'a> LossyWriter<'a> {
340 fn new(out: &'a mut [u8]) -> Self {
341 Self {
342 out,
343 pos: 0,
344 carry: [0; 4],
345 carry_len: 0,
346 truncated: false,
347 had_invalid: false,
348 }
349 }
350
351 fn emit(&mut self, bytes: &[u8]) {
352 let space = self.out.len() - self.pos;
353 if bytes.len() > space {
354 let mut take = space;
356 while take > 0 && bytes[take] & 0xC0 == 0x80 {
357 take -= 1;
358 }
359 self.out[self.pos..self.pos + take].copy_from_slice(&bytes[..take]);
360 self.pos += take;
361 self.truncated = true;
362 return;
363 }
364 self.out[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
365 self.pos += bytes.len();
366 }
367
368 fn emit_replacement(&mut self) {
369 self.had_invalid = true;
370 self.emit("\u{FFFD}".as_bytes());
371 }
372
373 fn push(&mut self, mut bytes: &[u8]) {
375 while self.carry_len > 0 && !bytes.is_empty() {
377 let needed = utf8_len(self.carry[0]).unwrap_or(1);
378 let take = (needed - self.carry_len).min(bytes.len());
379 self.carry[self.carry_len..self.carry_len + take].copy_from_slice(&bytes[..take]);
380 self.carry_len += take;
381 bytes = &bytes[take..];
382 if self.carry_len == needed {
383 let carried = self.carry;
384 let carry_len = self.carry_len;
385 self.carry_len = 0;
386 match core::str::from_utf8(&carried[..carry_len]) {
387 Ok(_) => self.emit(&carried[..carry_len]),
388 Err(_) => {
389 self.emit_replacement();
392 let rest = carry_len - 1;
393 let resume: [u8; 4] = carried;
394 self.push_inner(&resume[1..1 + rest]);
396 }
397 }
398 }
399 }
400 self.push_inner(bytes);
401 }
402
403 fn push_inner(&mut self, mut bytes: &[u8]) {
404 loop {
405 match core::str::from_utf8(bytes) {
406 Ok(text) => {
407 self.emit(text.as_bytes());
408 return;
409 }
410 Err(error) => {
411 let valid = error.valid_up_to();
412 self.emit(&bytes[..valid]);
413 match error.error_len() {
414 Some(bad) => {
415 self.emit_replacement();
416 bytes = &bytes[valid + bad..];
417 }
418 None => {
419 let tail = &bytes[valid..];
421 self.carry[..tail.len()].copy_from_slice(tail);
422 self.carry_len = tail.len();
423 return;
424 }
425 }
426 }
427 }
428 }
429 }
430
431 fn end_run(&mut self, at_gap: bool) {
435 if self.carry_len > 0 {
436 if !at_gap {
437 self.emit_replacement();
438 }
439 self.carry_len = 0;
440 }
441 }
442}
443
444fn utf8_len(lead: u8) -> Option<usize> {
445 match lead {
446 0x00..=0x7F => Some(1),
447 0xC0..=0xDF => Some(2),
448 0xE0..=0xEF => Some(3),
449 0xF0..=0xF4 => Some(4),
450 _ => None,
451 }
452}
453
454#[derive(Clone, Copy, Debug)]
456pub struct RenderSentinels {
457 pub pending: &'static str,
458 pub missing: &'static str,
459 pub unavailable: &'static str,
460}
461
462impl Default for RenderSentinels {
463 fn default() -> Self {
464 Self {
465 pending: "[PENDING]",
466 missing: "[MISSING]",
467 unavailable: "[UNAVAILABLE]",
468 }
469 }
470}
471
472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
474pub struct RenderResult {
475 pub len: usize,
476 pub complete: bool,
477 pub had_invalid: bool,
478 pub truncated: bool,
479}
480
481pub fn render_slot<const SLOTS: usize, const PAGES: usize>(
484 pool: &ReassemblyPool<SLOTS, PAGES>,
485 slot_index: usize,
486 sentinels: &RenderSentinels,
487 final_render: bool,
488 out: &mut [u8],
489) -> RenderResult {
490 let slot = pool.slots[slot_index].as_ref().expect("occupied slot");
491 let mut writer = LossyWriter::new(out);
492 let mut index = 0u8;
493 let mut after_gap = false;
494 while index < slot.count {
495 let bit = 1u16 << index;
496 if slot.present & bit != 0 {
497 let mut buffer = [0u8; FRAGMENT_BODY_MAX];
498 let len = pool.read_fragment(slot_index, index, &mut buffer);
499 let mut bytes = &buffer[..len];
500 if after_gap {
501 while let Some((first, rest)) = bytes.split_first() {
503 if first & 0xC0 == 0x80 {
504 bytes = rest;
505 } else {
506 break;
507 }
508 }
509 after_gap = false;
510 }
511 writer.push(bytes);
512 index += 1;
513 continue;
514 }
515 writer.end_run(true);
521 while index < slot.count && slot.present & (1u16 << index) == 0 {
522 let run_unavailable = slot.unavailable & (1u16 << index) != 0;
523 while index < slot.count
524 && slot.present & (1u16 << index) == 0
525 && (slot.unavailable & (1u16 << index) != 0) == run_unavailable
526 {
527 index += 1;
528 }
529 let sentinel = if run_unavailable {
530 sentinels.unavailable
531 } else if final_render {
532 sentinels.missing
533 } else {
534 sentinels.pending
535 };
536 writer.emit(sentinel.as_bytes());
537 }
538 after_gap = true;
539 }
540 writer.end_run(false);
541 RenderResult {
542 len: writer.pos,
543 complete: slot.is_complete(),
544 had_invalid: writer.had_invalid,
545 truncated: writer.truncated,
546 }
547}
548
549#[derive(Clone, Copy, Debug, PartialEq, Eq)]
551pub struct BodyTooLarge;
552
553#[derive(Clone, Copy, Debug, PartialEq, Eq)]
555pub struct FragmentPlan {
556 pub count: u8,
557 body_len: usize,
558}
559
560impl FragmentPlan {
561 pub fn plan(body_len: usize, single_frame_budget: usize) -> Result<Option<Self>, BodyTooLarge> {
565 if body_len <= single_frame_budget && body_len <= FRAGMENT_BODY_MAX {
566 return Ok(None);
567 }
568 let count = body_len.div_ceil(FRAGMENT_BODY_MAX);
569 if count > FRAGMENT_COUNT_MAX as usize || count < 2 {
570 if count < 2 {
571 return Ok(Some(Self { count: 2, body_len }));
573 }
574 return Err(BodyTooLarge);
575 }
576 Ok(Some(Self {
577 count: count as u8,
578 body_len,
579 }))
580 }
581
582 pub fn range(&self, index: u8) -> core::ops::Range<usize> {
584 let per = self
585 .body_len
586 .div_ceil(self.count as usize)
587 .min(FRAGMENT_BODY_MAX);
588 let start = per * index as usize;
589 let end = (start + per).min(self.body_len);
590 start..end
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::model::{ConversationKey, SenderScope};
598 use umsh_core::ChannelTag;
599
600 fn stream() -> StreamKey {
601 StreamKey {
602 conversation: ConversationKey::ChannelGroup {
603 channel: ChannelTag([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
604 },
605 sender: SenderScope::ClaimedMember(umsh_core::NodeHint([9, 9, 9])),
606 }
607 }
608
609 fn pool_with_slot(count: u8) -> (ReassemblyPool<4, 24>, usize) {
610 let mut pool = ReassemblyPool::<4, 24>::new();
611 let slot = empty_slot(stream(), 0, 7, count, MessageHandle(1), 0);
612 let index = pool.open_slot(slot).unwrap();
613 (pool, index)
614 }
615
616 #[test]
617 fn duplicate_and_conflict_detection() {
618 let (mut pool, index) = pool_with_slot(3);
619 assert_eq!(
620 pool.insert_fragment(index, 0, b"hello"),
621 InsertOutcome::Stored
622 );
623 assert_eq!(
624 pool.insert_fragment(index, 0, b"hello"),
625 InsertOutcome::Duplicate
626 );
627 assert_eq!(
628 pool.insert_fragment(index, 0, b"jello"),
629 InsertOutcome::Conflict
630 );
631 }
632
633 #[test]
634 fn partial_render_inserts_sentinel_and_skips_split_code_point() {
635 let (mut pool, index) = pool_with_slot(3);
636 pool.insert_fragment(index, 0, b"ab h\xC3").unwrap_stored();
638 pool.insert_fragment(index, 2, b"\xA9nd tail")
639 .unwrap_stored();
640 let mut out = [0u8; 128];
641 let result = render_slot(&pool, index, &RenderSentinels::default(), false, &mut out);
642 let text = core::str::from_utf8(&out[..result.len]).unwrap();
643 assert_eq!(text, "ab h[PENDING]nd tail");
644 assert!(!result.complete);
645 }
646
647 #[test]
648 fn complete_render_heals_boundary_code_points() {
649 let (mut pool, index) = pool_with_slot(2);
650 pool.insert_fragment(index, 0, b"h\xC3").unwrap_stored();
651 pool.insert_fragment(index, 1, b"\xA9!").unwrap_stored();
652 let mut out = [0u8; 64];
653 let result = render_slot(&pool, index, &RenderSentinels::default(), true, &mut out);
654 assert!(result.complete);
655 assert_eq!(core::str::from_utf8(&out[..result.len]).unwrap(), "hé!");
656 }
657
658 #[test]
659 fn oversized_fragment_is_rejected_not_truncated() {
660 let (mut pool, index) = pool_with_slot(2);
661 let big = [b'x'; FRAGMENT_BODY_MAX + 40];
662 assert_eq!(
663 pool.insert_fragment(index, 0, &big),
664 InsertOutcome::TooLarge
665 );
666 assert_eq!(pool.free_pages(), 24);
668 assert!(pool.slots[index].as_ref().unwrap().present == 0);
669 }
670
671 #[test]
672 fn split_absent_run_renders_one_sentinel_per_repair_state() {
673 let (mut pool, index) = pool_with_slot(5);
674 pool.insert_fragment(index, 0, b"a").unwrap_stored();
675 pool.insert_fragment(index, 4, b"z").unwrap_stored();
676 pool.slots[index].as_mut().unwrap().unavailable = 1 << 2;
678 let mut out = [0u8; 128];
679 let result = render_slot(&pool, index, &RenderSentinels::default(), false, &mut out);
680 let text = core::str::from_utf8(&out[..result.len]).unwrap();
681 assert_eq!(text, "a[PENDING][UNAVAILABLE][PENDING]z");
682
683 let result = render_slot(&pool, index, &RenderSentinels::default(), true, &mut out);
685 let text = core::str::from_utf8(&out[..result.len]).unwrap();
686 assert_eq!(text, "a[MISSING][UNAVAILABLE][MISSING]z");
687 }
688
689 #[test]
690 fn pages_recycle_after_close() {
691 let (mut pool, index) = pool_with_slot(2);
692 let big = [b'x'; 160];
693 pool.insert_fragment(index, 0, &big).unwrap_stored();
694 pool.insert_fragment(index, 1, &big).unwrap_stored();
695 let free_before = pool.free_pages();
696 pool.close_slot(index);
697 assert!(pool.free_pages() > free_before);
698 assert_eq!(pool.free_pages(), 24);
699 }
700
701 trait UnwrapStored {
702 fn unwrap_stored(self);
703 }
704 impl UnwrapStored for InsertOutcome {
705 fn unwrap_stored(self) {
706 assert_eq!(self, InsertOutcome::Stored);
707 }
708 }
709
710 #[test]
711 fn fragment_plan_ranges_cover_body() {
712 let plan = FragmentPlan::plan(400, 200).unwrap().unwrap();
713 assert_eq!(plan.count, 3);
714 let mut covered = 0;
715 for index in 0..plan.count {
716 let range = plan.range(index);
717 assert_eq!(range.start, covered);
718 covered = range.end;
719 assert!(range.len() <= FRAGMENT_BODY_MAX);
720 }
721 assert_eq!(covered, 400);
722 }
723}