use std::ops::ControlFlow;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkEvent {
pub index: usize,
pub kind: [u8; 4],
pub bytes_read: u64,
}
impl ChunkEvent {
#[must_use]
pub fn new(index: usize, kind: [u8; 4], bytes_read: u64) -> Self {
Self {
index,
kind,
bytes_read,
}
}
#[must_use]
pub fn kind_str(&self) -> Option<&str> {
std::str::from_utf8(&self.kind).ok()
}
}
pub trait ApplyObserver {
fn on_chunk_applied(&mut self, ev: ChunkEvent) -> ControlFlow<(), ()> {
let _ = ev;
ControlFlow::Continue(())
}
fn should_cancel(&mut self) -> bool {
false
}
}
impl<F> ApplyObserver for F
where
F: FnMut(ChunkEvent) -> ControlFlow<(), ()>,
{
fn on_chunk_applied(&mut self, ev: ChunkEvent) -> ControlFlow<(), ()> {
self(ev)
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopObserver;
impl ApplyObserver for NoopObserver {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_observer_on_chunk_applied_always_continues() {
let mut obs = NoopObserver;
let ev = ChunkEvent::new(0, *b"ADIR", 32);
assert_eq!(
obs.on_chunk_applied(ev),
ControlFlow::Continue(()),
"NoopObserver must never break"
);
}
#[test]
fn noop_observer_should_cancel_always_false() {
let mut obs = NoopObserver;
for _ in 0..5 {
assert!(
!obs.should_cancel(),
"NoopObserver must never request cancellation"
);
}
}
#[test]
fn closure_observer_accumulates_events_via_closure_mutation() {
let mut seen: Vec<ChunkEvent> = Vec::new();
{
let mut obs = |ev| {
seen.push(ev);
ControlFlow::Continue(())
};
assert_eq!(
obs.on_chunk_applied(ChunkEvent::new(0, *b"ADIR", 32)),
ControlFlow::Continue(()),
"first call must continue"
);
assert_eq!(
obs.on_chunk_applied(ChunkEvent::new(1, *b"SQPK", 96)),
ControlFlow::Continue(()),
"second call must continue"
);
}
assert_eq!(seen.len(), 2, "exactly two events must have been captured");
assert_eq!(seen[0].index, 0);
assert_eq!(seen[0].kind, *b"ADIR");
assert_eq!(seen[0].bytes_read, 32);
assert_eq!(seen[1].index, 1);
assert_eq!(seen[1].kind, *b"SQPK");
assert_eq!(seen[1].bytes_read, 96);
}
#[test]
fn closure_observer_break_propagates_to_caller() {
let mut obs = |_| ControlFlow::Break(());
assert_eq!(
obs.on_chunk_applied(ChunkEvent::new(0, *b"SQPK", 16)),
ControlFlow::Break(()),
"Break from closure must propagate through the blanket impl"
);
}
#[test]
fn closure_observer_break_after_n_events() {
let mut count = 0usize;
let mut obs = |_| {
count += 1;
if count < 3 {
ControlFlow::Continue(())
} else {
ControlFlow::Break(())
}
};
assert_eq!(
obs.on_chunk_applied(ChunkEvent::new(0, *b"ADIR", 10)),
ControlFlow::Continue(())
);
assert_eq!(
obs.on_chunk_applied(ChunkEvent::new(1, *b"ADIR", 20)),
ControlFlow::Continue(())
);
assert_eq!(
obs.on_chunk_applied(ChunkEvent::new(2, *b"SQPK", 30)),
ControlFlow::Break(()),
"third call must break"
);
}
#[test]
fn closure_observer_should_cancel_always_false() {
let mut obs = |_| ControlFlow::Continue(());
for _ in 0..3 {
assert!(
!obs.should_cancel(),
"closure observer must never cancel mid-chunk"
);
}
}
#[test]
fn chunk_event_new_stores_all_fields_exactly() {
let ev = ChunkEvent::new(7, *b"SQPK", 1024);
assert_eq!(ev.index, 7, "index field mismatch");
assert_eq!(ev.kind, *b"SQPK", "kind field mismatch");
assert_eq!(ev.bytes_read, 1024, "bytes_read field mismatch");
}
#[test]
fn chunk_event_clone_and_eq_are_consistent() {
let ev = ChunkEvent::new(3, *b"ADIR", 512);
let cloned = ev;
assert_eq!(
ev, cloned,
"ChunkEvent must be Copy/Eq with field-wise equality"
);
}
#[test]
fn chunk_event_kind_str_returns_some_for_ascii_tag() {
let ev = ChunkEvent::new(0, *b"ADIR", 0);
assert_eq!(ev.kind_str(), Some("ADIR"));
let ev = ChunkEvent::new(0, *b"SQPK", 0);
assert_eq!(ev.kind_str(), Some("SQPK"));
}
#[test]
fn chunk_event_kind_str_returns_none_for_invalid_utf8() {
let ev = ChunkEvent::new(0, [0xFF, 0xFE, 0xFD, 0xFC], 0);
assert_eq!(ev.kind_str(), None, "non-UTF-8 tag bytes must produce None");
}
}