use std::io::IoSlice;
use kawa::{AsBuffer, Kawa};
pub unsafe fn gather<T: AsBuffer>(kawa: &Kawa<T>, io_slices: &mut Vec<IoSlice<'static>>) -> usize {
io_slices.clear();
io_slices.reserve(kawa.out.len());
unsafe { push_out_blocks(kawa, io_slices) }
}
pub unsafe fn gather_after<T: AsBuffer>(
prefix: &[u8],
kawa: &Kawa<T>,
io_slices: &mut Vec<IoSlice<'static>>,
) -> (usize, usize) {
io_slices.clear();
io_slices.reserve(kawa.out.len() + usize::from(!prefix.is_empty()));
if !prefix.is_empty() {
let prefix: &'static [u8] =
unsafe { std::slice::from_raw_parts(prefix.as_ptr(), prefix.len()) };
io_slices.push(IoSlice::new(prefix));
}
let stream = unsafe { push_out_blocks(kawa, io_slices) };
(prefix.len(), prefix.len() + stream)
}
unsafe fn push_out_blocks<T: AsBuffer>(
kawa: &Kawa<T>,
io_slices: &mut Vec<IoSlice<'static>>,
) -> usize {
let already = io_slices.len();
let buffer = kawa.storage.buffer();
let mut bytes_offered = 0usize;
for block in kawa.out.iter() {
match block {
kawa::OutBlock::Delimiter => break,
kawa::OutBlock::Store(store) => {
let data = store.data(buffer);
let data: &'static [u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
bytes_offered += data.len();
io_slices.push(IoSlice::new(data));
}
}
}
debug_assert_eq!(
io_slices[already..].iter().map(|s| s.len()).sum::<usize>(),
bytes_offered,
"the reported offer must equal the bytes the descriptors describe"
);
debug_assert!(
bytes_offered == 0 || io_slices.len() > already,
"a non-zero offer must be backed by at least one descriptor"
);
bytes_offered
}
pub fn frame_tail(slices: &[IoSlice], written: usize) -> usize {
frame_tail_walk(slices, written).0
}
struct SliceCursor<'a, 'b> {
slices: &'a [IoSlice<'b>],
index: usize,
offset: usize,
steps: usize,
}
impl SliceCursor<'_, '_> {
fn skip(&mut self, mut count: usize) -> bool {
while count > 0 {
let Some(slice) = self.slices.get(self.index) else {
return false;
};
let left = slice.len() - self.offset;
if count < left {
self.offset += count;
return true;
}
count -= left;
self.index += 1;
self.offset = 0;
self.steps += 1;
}
true
}
fn next_byte(&mut self) -> Option<u8> {
loop {
let slice = self.slices.get(self.index)?;
if self.offset < slice.len() {
let byte = slice[self.offset];
self.skip(1);
return Some(byte);
}
self.index += 1;
self.offset = 0;
self.steps += 1;
}
}
}
fn frame_tail_walk(slices: &[IoSlice], written: usize) -> (usize, usize) {
const HEADERS: u8 = 0x1;
const PUSH_PROMISE: u8 = 0x5;
const CONTINUATION: u8 = 0x9;
const END_HEADERS: u8 = 0x4;
const FRAME_HEADER: usize = 9;
let total: usize = slices.iter().map(|slice| slice.len()).sum();
let mut cursor = SliceCursor {
slices,
index: 0,
offset: 0,
steps: 0,
};
let mut boundary = 0usize;
let mut in_block = false;
loop {
if boundary >= written && !in_block {
return (boundary - written, cursor.steps);
}
if boundary + FRAME_HEADER > total {
debug_assert!(false, "a frame runs past the gathered bytes");
return (total.saturating_sub(written), cursor.steps);
}
let mut head = [0u8; 5];
for byte in &mut head {
*byte = cursor.next_byte().unwrap_or(0);
}
let payload_len =
(usize::from(head[0]) << 16) | (usize::from(head[1]) << 8) | usize::from(head[2]);
in_block =
matches!(head[3], HEADERS | PUSH_PROMISE | CONTINUATION) && head[4] & END_HEADERS == 0;
boundary += FRAME_HEADER + payload_len;
if boundary > total || !cursor.skip(FRAME_HEADER - head.len() + payload_len) {
debug_assert!(false, "a frame runs past the gathered bytes");
return (total.saturating_sub(written), cursor.steps);
}
}
}
pub fn holds_header_frame<T: AsBuffer>(kawa: &Kawa<T>) -> bool {
const HEADERS: u8 = 0x1;
const PUSH_PROMISE: u8 = 0x5;
const CONTINUATION: u8 = 0x9;
let mut found = false;
let tiled = walk_whole_frames(kawa, |frame_type, _| {
found = matches!(frame_type, HEADERS | PUSH_PROMISE | CONTINUATION);
!found
});
found || !tiled
}
pub fn queued_data_payload<T: AsBuffer>(kawa: &Kawa<T>) -> usize {
const DATA: u8 = 0x0;
let mut payload = 0usize;
let tiled = walk_whole_frames(kawa, |frame_type, payload_len| {
if frame_type == DATA {
payload += payload_len;
}
true
});
debug_assert!(tiled, "an H2 stream queues whole frames only");
debug_assert!(
payload <= kawa.out.iter().map(|block| block_len(kawa, block)).sum(),
"the counted DATA payload is part of the queued octets"
);
payload
}
fn block_len<T: AsBuffer>(kawa: &Kawa<T>, block: &kawa::OutBlock) -> usize {
match block {
kawa::OutBlock::Delimiter => 0,
kawa::OutBlock::Store(store) => store.data(kawa.storage.buffer()).len(),
}
}
fn walk_whole_frames<T: AsBuffer>(
kawa: &Kawa<T>,
mut visit: impl FnMut(u8, usize) -> bool,
) -> bool {
const FRAME_HEADER: usize = 9;
let buffer = kawa.storage.buffer();
let mut head = [0u8; FRAME_HEADER];
let mut filled = 0usize;
let mut payload_left = 0usize;
for block in kawa.out.iter() {
let kawa::OutBlock::Store(store) = block else {
break;
};
let mut data = store.data(buffer);
while !data.is_empty() {
if payload_left > 0 {
let skipped = payload_left.min(data.len());
payload_left -= skipped;
data = &data[skipped..];
if payload_left == 0 && !visit(head[3], frame_payload_len(&head)) {
return true;
}
continue;
}
let copied = (FRAME_HEADER - filled).min(data.len());
head[filled..filled + copied].copy_from_slice(&data[..copied]);
filled += copied;
data = &data[copied..];
if filled == FRAME_HEADER {
filled = 0;
payload_left = frame_payload_len(&head);
if payload_left == 0 && !visit(head[3], 0) {
return true;
}
}
}
}
debug_assert!(
filled < FRAME_HEADER,
"a whole frame header is consumed as soon as it is read"
);
filled == 0 && payload_left == 0
}
fn frame_payload_len(head: &[u8; 9]) -> usize {
(usize::from(head[0]) << 16) | (usize::from(head[1]) << 8) | usize::from(head[2])
}
pub fn confirm<T: AsBuffer>(
kawa: &mut Kawa<T>,
io_slices: &mut Vec<IoSlice<'static>>,
size: usize,
) {
io_slices.clear();
debug_assert!(
io_slices.is_empty(),
"IoSlice refs must be cleared before consume"
);
kawa.consume(size);
}
#[cfg(test)]
mod tests {
use super::*;
use kawa::{Buffer, Kind, SliceBuffer, Store};
fn kawa_with_out<'a>(
buf: &'a mut [u8],
payload: &[u8],
splits: &[usize],
) -> Kawa<SliceBuffer<'a>> {
let mut kawa = Kawa::new(Kind::Response, Buffer::new(SliceBuffer(buf)));
kawa.storage.space()[..payload.len()].copy_from_slice(payload);
kawa.storage.fill(payload.len());
kawa.storage.head = payload.len();
let mut start = 0usize;
for &end in splits {
kawa.out
.push_back(kawa::OutBlock::Store(Store::Slice(kawa::repr::Slice {
start: start as u32,
len: (end - start) as u32,
})));
start = end;
}
if start < payload.len() {
kawa.out
.push_back(kawa::OutBlock::Store(Store::Slice(kawa::repr::Slice {
start: start as u32,
len: (payload.len() - start) as u32,
})));
}
kawa
}
fn gathered_bytes(io_slices: &[IoSlice<'static>]) -> Vec<u8> {
io_slices.iter().flat_map(|s| s.to_vec()).collect()
}
#[test]
fn gather_describes_every_queued_block_in_order() {
let mut buf = vec![0u8; 256];
let payload = b"abcdefghijklmnop";
let mut kawa = kawa_with_out(&mut buf, payload, &[4, 9]);
let mut slices: Vec<IoSlice<'static>> = Vec::new();
let offered = unsafe { gather(&kawa, &mut slices) };
assert_eq!(offered, payload.len());
assert_eq!(slices.len(), 3, "three blocks, three descriptors");
assert_eq!(gathered_bytes(&slices), payload.to_vec());
confirm(&mut kawa, &mut slices, offered);
}
#[test]
fn gather_after_offers_the_prefix_first_and_only_when_there_is_one() {
let mut buf = vec![0u8; 256];
let payload = b"abcdefghijklmnop";
let mut kawa = kawa_with_out(&mut buf, payload, &[4, 9]);
let mut slices: Vec<IoSlice<'static>> = Vec::new();
let prefix = b"queued";
let (prefix_len, offered) = unsafe { gather_after(prefix, &kawa, &mut slices) };
assert_eq!(
(prefix_len, offered),
(prefix.len(), prefix.len() + payload.len())
);
assert_eq!(slices.len(), 4, "the prefix, then the three blocks");
let mut expected = prefix.to_vec();
expected.extend_from_slice(payload);
assert_eq!(gathered_bytes(&slices), expected);
slices.clear();
let (prefix_len, offered) = unsafe { gather_after(b"", &kawa, &mut slices) };
assert_eq!((prefix_len, offered), (0, payload.len()));
assert_eq!(slices.len(), 3, "no descriptor for an empty prefix");
confirm(&mut kawa, &mut slices, offered);
}
#[test]
fn gather_on_an_empty_queue_offers_nothing() {
let mut buf = vec![0u8; 64];
let mut kawa = Kawa::new(Kind::Response, Buffer::new(SliceBuffer(&mut buf)));
let mut slices: Vec<IoSlice<'static>> = Vec::new();
assert_eq!(unsafe { gather(&kawa, &mut slices) }, 0);
assert!(slices.is_empty());
confirm(&mut kawa, &mut slices, 0);
}
#[test]
fn gather_stops_at_a_delimiter() {
let mut buf = vec![0u8; 256];
let payload = b"abcdefgh";
let mut kawa = Kawa::new(Kind::Response, Buffer::new(SliceBuffer(&mut buf)));
kawa.storage.space()[..payload.len()].copy_from_slice(payload);
kawa.storage.fill(payload.len());
kawa.out
.push_back(kawa::OutBlock::Store(Store::Slice(kawa::repr::Slice {
start: 0,
len: 4,
})));
kawa.out.push_back(kawa::OutBlock::Delimiter);
kawa.out
.push_back(kawa::OutBlock::Store(Store::Slice(kawa::repr::Slice {
start: 4,
len: 4,
})));
let mut slices: Vec<IoSlice<'static>> = Vec::new();
let offered = unsafe { gather(&kawa, &mut slices) };
assert_eq!(offered, 4, "the delimiter bounds the offer");
assert_eq!(gathered_bytes(&slices), b"abcd".to_vec());
confirm(&mut kawa, &mut slices, 0);
}
#[test]
fn confirm_leaves_no_descriptor_for_the_next_round() {
let mut buf = vec![0u8; 256];
let payload = b"abcdefgh";
let mut kawa = kawa_with_out(&mut buf, payload, &[4]);
let mut slices: Vec<IoSlice<'static>> = Vec::new();
unsafe { gather(&kawa, &mut slices) };
assert!(!slices.is_empty(), "premise: the gather produced something");
confirm(&mut kawa, &mut slices, 4);
assert!(
slices.is_empty(),
"confirm must leave no descriptor for the next round"
);
}
mod partial_write_property {
use super::*;
use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
const MAX_ROUNDS: usize = 512;
#[derive(Clone, Debug)]
struct WritePlan {
payload: Vec<u8>,
block_splits: Vec<usize>,
delimiter_after: Vec<usize>,
chunk_sizes: Vec<usize>,
}
impl Arbitrary for WritePlan {
fn arbitrary(g: &mut Gen) -> Self {
let len = 1 + usize::arbitrary(g) % 64;
let payload: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
let block_count = 1 + usize::arbitrary(g) % 4;
let mut block_splits: Vec<usize> = (0..block_count.saturating_sub(1))
.map(|_| 1 + usize::arbitrary(g) % len)
.collect();
block_splits.sort_unstable();
block_splits.dedup();
block_splits.retain(|&s| s < len);
let delimiter_after: Vec<usize> = (0..usize::arbitrary(g) % 3)
.map(|_| usize::arbitrary(g) % (block_splits.len() + 1))
.collect();
let chunk_sizes: Vec<usize> = (0..1 + usize::arbitrary(g) % 4)
.map(|_| usize::arbitrary(g) % (len + 1))
.collect();
WritePlan {
payload,
block_splits,
delimiter_after,
chunk_sizes,
}
}
}
fn kawa_for<'a>(buf: &'a mut [u8], plan: &WritePlan) -> Kawa<SliceBuffer<'a>> {
let payload = &plan.payload;
let mut kawa = Kawa::new(Kind::Response, Buffer::new(SliceBuffer(buf)));
kawa.storage.space()[..payload.len()].copy_from_slice(payload);
kawa.storage.fill(payload.len());
kawa.storage.head = payload.len();
let mut bounds: Vec<usize> = plan.block_splits.clone();
bounds.push(payload.len());
let mut start = 0usize;
for (index, &end) in bounds.iter().enumerate() {
if end > start {
kawa.out
.push_back(kawa::OutBlock::Store(Store::Slice(kawa::repr::Slice {
start: start as u32,
len: (end - start) as u32,
})));
start = end;
}
if plan.delimiter_after.contains(&index) {
kawa.out.push_back(kawa::OutBlock::Delimiter);
}
}
kawa
}
fn drive(plan: &WritePlan) -> (Vec<u8>, bool) {
let mut buf = vec![0u8; plan.payload.len() * 4 + 64];
let mut kawa = kawa_for(&mut buf, plan);
let mut slices: Vec<IoSlice<'static>> = Vec::new();
let mut written: Vec<u8> = Vec::new();
let mut tick = 0usize;
let mut rounds = 0usize;
while !kawa.out.is_empty() {
rounds += 1;
assert!(
rounds <= MAX_ROUNDS,
"drive must terminate within its bound"
);
let offered = unsafe { gather(&kawa, &mut slices) };
let accept = plan.chunk_sizes[tick % plan.chunk_sizes.len()].min(offered);
tick += 1;
let mut taken = 0usize;
for slice in slices.iter() {
if taken >= accept {
break;
}
let take = (accept - taken).min(slice.len());
written.extend_from_slice(&slice[..take]);
taken += take;
}
confirm(&mut kawa, &mut slices, accept);
if accept == 0 && offered > 0 {
return (written, false);
}
}
(written, true)
}
quickcheck! {
fn qc_partial_writes_preserve_the_byte_stream(plan: WritePlan) -> TestResult {
if plan.payload.is_empty() || plan.chunk_sizes.is_empty() {
return TestResult::discard();
}
let (written, drained) = drive(&plan);
if written.len() > plan.payload.len()
|| written != plan.payload[..written.len()]
{
return TestResult::error(
"the bytes handed to the shell are not a prefix of the payload",
);
}
if drained && written != plan.payload {
return TestResult::error(
"the queue drained but the payload was not fully written",
);
}
TestResult::passed()
}
}
}
mod frame_tail_cases {
use std::io::IoSlice;
use super::super::{frame_tail, frame_tail_walk};
fn frame(kind: u8, flags: u8, payload: &[u8]) -> Vec<u8> {
let len = payload.len();
let mut frame = vec![(len >> 16) as u8, (len >> 8) as u8, len as u8, kind, flags];
frame.extend_from_slice(&1u32.to_be_bytes());
frame.extend_from_slice(payload);
frame
}
fn tail(blocks: &[&[u8]], written: usize) -> usize {
let slices: Vec<IoSlice> = blocks.iter().map(|block| IoSlice::new(block)).collect();
frame_tail(&slices, written)
}
#[test]
fn a_write_on_a_frame_boundary_leaves_no_tail() {
let data = frame(0, 0, b"abcd");
let rst = frame(3, 0, &8u32.to_be_bytes());
assert_eq!(tail(&[&data, &rst], 0), 0);
assert_eq!(tail(&[&data, &rst], data.len()), 0);
assert_eq!(tail(&[&data, &rst], data.len() + rst.len()), 0);
}
#[test]
fn a_write_inside_a_frame_owes_the_rest_of_that_frame_only() {
let data = frame(0, 0, b"abcdef");
let rst = frame(3, 0, &8u32.to_be_bytes());
assert_eq!(tail(&[&data, &rst], 4), data.len() - 4, "inside the header");
assert_eq!(
tail(&[&data, &rst], 11),
data.len() - 11,
"inside the payload"
);
assert_eq!(
tail(&[&data, &rst], data.len() + 1),
rst.len() - 1,
"inside the RST"
);
}
#[test]
fn a_frame_header_split_across_blocks_is_read_whole() {
let data = frame(0, 0, b"abcdef");
let (head, rest) = data.split_at(4);
assert_eq!(tail(&[head, rest], 2), data.len() - 2);
assert_eq!(tail(&[head, rest], 12), data.len() - 12);
}
#[test]
fn an_empty_end_stream_data_frame_is_a_whole_frame() {
let end = frame(0, 0x1, b"");
assert_eq!(tail(&[&end], 5), 4);
assert_eq!(tail(&[&end], 9), 0);
}
#[test]
fn a_cut_header_block_is_owed_up_to_end_headers() {
let headers = frame(1, 0, b"hpack-1");
let continuation = frame(9, 0, b"hpack-2");
let last = frame(9, 0x4, b"hpack-3");
let data = frame(0, 0x1, b"body");
let blocks: [&[u8]; 4] = [&headers, &continuation, &last, &data];
let block_len = headers.len() + continuation.len() + last.len();
assert_eq!(tail(&blocks, 3), block_len - 3, "inside the HEADERS");
assert_eq!(
tail(&blocks, headers.len()),
block_len - headers.len(),
"on the HEADERS/CONTINUATION boundary"
);
assert_eq!(tail(&blocks, block_len), 0, "after END_HEADERS");
assert_eq!(tail(&blocks, block_len + 2), data.len() - 2);
}
#[test]
fn the_walk_is_linear_in_the_slices() {
const FRAMES: usize = 2700;
let frames: Vec<Vec<u8>> = (0..FRAMES).map(|_| frame(0, 0, b"x")).collect();
let mut blocks: Vec<&[u8]> = Vec::with_capacity(FRAMES * 2);
for frame in &frames {
let (head, payload) = frame.split_at(9);
blocks.push(head);
blocks.push(payload);
}
let slices: Vec<IoSlice> = blocks.iter().map(|block| IoSlice::new(block)).collect();
let total = FRAMES * 10;
let (tail, steps) = frame_tail_walk(&slices, total - 5);
assert_eq!(tail, 5, "the last frame is cut 5 bytes before its end");
assert!(
steps <= slices.len(),
"the walk stepped past {steps} slices for {} slices",
slices.len()
);
}
#[test]
fn an_end_headers_headers_frame_closes_its_block() {
let headers = frame(1, 0x4 | 0x1, b"hpack");
let rst = frame(3, 0, &8u32.to_be_bytes());
assert_eq!(tail(&[&headers, &rst], headers.len()), 0);
}
}
#[test]
fn holds_header_frame_finds_a_block_behind_other_frames() {
let data = [0, 0, 3, 0, 0, 0, 0, 0, 1, b'a', b'b', b'c'];
let rst = [0, 0, 4, 3, 0, 0, 0, 0, 1, 0, 0, 0, 8];
let trailers = [0, 0, 1, 1, 5, 0, 0, 0, 1, 0x88];
let mut frames = Vec::new();
frames.extend_from_slice(&data);
frames.extend_from_slice(&rst);
let no_block = frames.len();
frames.extend_from_slice(&trailers);
for cut in 1..frames.len() {
let mut buf = vec![0u8; 64];
let kawa = kawa_with_out(&mut buf, &frames, &[cut]);
let before = crate::test_allocations::allocations();
assert!(holds_header_frame(&kawa), "cut at {cut}");
assert_eq!(
crate::test_allocations::allocations() - before,
0,
"the walk allocates nothing"
);
}
for cut in 1..no_block {
let mut buf = vec![0u8; 64];
let kawa = kawa_with_out(&mut buf, &frames[..no_block], &[cut]);
assert!(!holds_header_frame(&kawa), "cut at {cut}");
}
let mut buf = vec![0u8; 64];
let kawa = kawa_with_out(&mut buf, &data[..5], &[]);
assert!(
holds_header_frame(&kawa),
"a cut frame answers the safe side"
);
}
#[test]
fn queued_data_payload_counts_only_data_payload_octets() {
let data = [0, 0, 3, 0, 0, 0, 0, 0, 1, b'a', b'b', b'c'];
let rst = [0, 0, 4, 3, 0, 0, 0, 0, 1, 0, 0, 0, 8];
let trailers = [0, 0, 1, 1, 5, 0, 0, 0, 1, 0x88];
let more = [0, 0, 5, 0, 0, 0, 0, 0, 1, b'd', b'e', b'f', b'g', b'h'];
let end = [0, 0, 0, 0, 1, 0, 0, 0, 1];
let mut frames = Vec::new();
for frame in [&data[..], &rst, &trailers, &more, &end] {
frames.extend_from_slice(frame);
}
for cut in 1..frames.len() {
let mut buf = vec![0u8; 64];
let kawa = kawa_with_out(&mut buf, &frames, &[cut]);
let before = crate::test_allocations::allocations();
assert_eq!(queued_data_payload(&kawa), 3 + 5, "cut at {cut}");
assert_eq!(
crate::test_allocations::allocations() - before,
0,
"the walk allocates nothing"
);
}
let mut buf = vec![0u8; 64];
let kawa = kawa_with_out(&mut buf, &rst, &[]);
assert_eq!(queued_data_payload(&kawa), 0, "no DATA, no credit");
}
}