Handle overlapping stream receive chunks
ci / test (push) Waiting to run
ci / fuzz-smoke (push) Waiting to run
ci / windows-client (push) Waiting to run
ci / package-release (linux-x86_64, ubuntu-latest) (push) Waiting to run
ci / package-release (macos-aarch64, macos-14) (push) Waiting to run
ci / package-release (macos-x86_64, macos-13) (push) Waiting to run
ci / package-release (windows-x86_64, windows-latest) (push) Waiting to run
ci / publish-gitea-release (push) Blocked by required conditions
ci / remote-bench (push) Waiting to run

This commit is contained in:
DuProcess
2026-07-13 00:05:30 -04:00
parent 3ab314698b
commit dccd648ac9
3 changed files with 177 additions and 124 deletions
+6 -40
View File
@@ -8124,48 +8124,14 @@ fn accept_stream_data(
stream_recv_pending: &mut HashMap<u64, BTreeMap<u64, Vec<u8>>>,
data: StreamData,
) -> (Vec<Vec<u8>>, usize, u64) {
let stream_id = data.stream_id;
let expected = stream_next_recv_offset.entry(stream_id).or_insert(0);
if !dosh::transport::stream_data_within_receive_window(
*expected,
dosh::transport::accept_stream_data_chunks(
stream_next_recv_offset,
stream_recv_pending,
data.stream_id,
data.offset,
data.bytes.len(),
data.bytes,
STREAM_INITIAL_WINDOW,
) {
return (Vec::new(), 0, *expected);
}
if data.offset < *expected {
return (Vec::new(), 0, *expected);
}
if data.offset > *expected {
stream_recv_pending
.entry(stream_id)
.or_default()
.entry(data.offset)
.or_insert(data.bytes);
return (Vec::new(), 0, *expected);
}
let mut writes = vec![data.bytes];
let mut consumed = writes[0].len();
*expected = expected.saturating_add(consumed as u64);
while let Some(bytes) = stream_recv_pending
.entry(stream_id)
.or_default()
.remove(expected)
{
consumed = consumed.saturating_add(bytes.len());
*expected = expected.saturating_add(bytes.len() as u64);
writes.push(bytes);
}
if stream_recv_pending
.get(&stream_id)
.is_some_and(BTreeMap::is_empty)
{
stream_recv_pending.remove(&stream_id);
}
(writes, consumed, *expected)
)
}
#[allow(clippy::too_many_arguments)]
+6 -43
View File
@@ -3523,51 +3523,14 @@ async fn flush_stream_pending_data_to_client(
}
fn accept_stream_data(client: &mut ClientState, data: StreamData) -> (Vec<Vec<u8>>, usize, u64) {
let stream_id = data.stream_id;
let expected = client.stream_next_recv_offset.entry(stream_id).or_insert(0);
if !dosh::transport::stream_data_within_receive_window(
*expected,
dosh::transport::accept_stream_data_chunks(
&mut client.stream_next_recv_offset,
&mut client.stream_recv_pending,
data.stream_id,
data.offset,
data.bytes.len(),
data.bytes,
STREAM_INITIAL_WINDOW,
) {
return (Vec::new(), 0, *expected);
}
if data.offset < *expected {
return (Vec::new(), 0, *expected);
}
if data.offset > *expected {
client
.stream_recv_pending
.entry(stream_id)
.or_default()
.entry(data.offset)
.or_insert(data.bytes);
return (Vec::new(), 0, *expected);
}
let mut writes = vec![data.bytes];
let mut consumed = writes[0].len();
*expected = expected.saturating_add(consumed as u64);
while let Some(bytes) = client
.stream_recv_pending
.entry(stream_id)
.or_default()
.remove(expected)
{
consumed += bytes.len();
*expected = expected.saturating_add(bytes.len() as u64);
writes.push(bytes);
}
if client
.stream_recv_pending
.get(&stream_id)
.is_some_and(BTreeMap::is_empty)
{
client.stream_recv_pending.remove(&stream_id);
}
(writes, consumed, *expected)
)
}
fn cleanup_client_stream(client: &mut ClientState, stream_id: u64) {
+165 -41
View File
@@ -654,49 +654,14 @@ impl StreamMux {
}
fn accept_data(&mut self, data: StreamData) -> (Vec<Vec<u8>>, usize, u64) {
let stream_id = data.stream_id;
let expected = self.next_recv_offset.entry(stream_id).or_insert(0);
if !stream_data_within_receive_window(
*expected,
accept_stream_data_chunks(
&mut self.next_recv_offset,
&mut self.recv_pending,
data.stream_id,
data.offset,
data.bytes.len(),
data.bytes,
self.config.initial_window,
) {
return (Vec::new(), 0, *expected);
}
if data.offset < *expected {
return (Vec::new(), 0, *expected);
}
if data.offset > *expected {
self.recv_pending
.entry(stream_id)
.or_default()
.entry(data.offset)
.or_insert(data.bytes);
return (Vec::new(), 0, *expected);
}
let mut chunks = vec![data.bytes];
let mut consumed = chunks[0].len();
*expected = expected.saturating_add(consumed as u64);
while let Some(bytes) = self
.recv_pending
.entry(stream_id)
.or_default()
.remove(expected)
{
consumed = consumed.saturating_add(bytes.len());
*expected = expected.saturating_add(bytes.len() as u64);
chunks.push(bytes);
}
if self
.recv_pending
.get(&stream_id)
.is_some_and(BTreeMap::is_empty)
{
self.recv_pending.remove(&stream_id);
}
(chunks, consumed, *expected)
)
}
fn ack_data(&mut self, stream_id: u64, received_offset: u64) -> usize {
@@ -822,6 +787,92 @@ pub fn stream_data_within_receive_window(
end <= expected_offset.saturating_add(window)
}
fn trim_stream_data_to_offset(
offset: u64,
bytes: Vec<u8>,
expected_offset: u64,
) -> Option<(u64, Vec<u8>)> {
let len = u64::try_from(bytes.len()).ok()?;
let end = offset.checked_add(len)?;
if end <= expected_offset {
return None;
}
if offset >= expected_offset {
return Some((offset, bytes));
}
let skip = usize::try_from(expected_offset - offset).ok()?;
Some((expected_offset, bytes.get(skip..)?.to_vec()))
}
fn insert_pending_stream_data(pending: &mut BTreeMap<u64, Vec<u8>>, offset: u64, bytes: Vec<u8>) {
match pending.entry(offset) {
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(bytes);
}
std::collections::btree_map::Entry::Occupied(mut entry) => {
if bytes.len() > entry.get().len() {
entry.insert(bytes);
}
}
}
}
pub fn accept_stream_data_chunks(
next_recv_offset: &mut HashMap<u64, u64>,
recv_pending: &mut HashMap<u64, BTreeMap<u64, Vec<u8>>>,
stream_id: u64,
offset: u64,
bytes: Vec<u8>,
receive_window: usize,
) -> (Vec<Vec<u8>>, usize, u64) {
let expected = next_recv_offset.entry(stream_id).or_insert(0);
let Some((offset, bytes)) = trim_stream_data_to_offset(offset, bytes, *expected) else {
return (Vec::new(), 0, *expected);
};
if !stream_data_within_receive_window(*expected, offset, bytes.len(), receive_window) {
return (Vec::new(), 0, *expected);
}
if offset > *expected {
insert_pending_stream_data(recv_pending.entry(stream_id).or_default(), offset, bytes);
return (Vec::new(), 0, *expected);
}
let mut chunks = vec![bytes];
let mut consumed = chunks[0].len();
*expected = expected.saturating_add(consumed as u64);
while let Some((&pending_offset, _)) = recv_pending
.get(&stream_id)
.and_then(|pending| pending.range(..=*expected).next_back())
{
let Some(bytes) = recv_pending
.get_mut(&stream_id)
.and_then(|pending| pending.remove(&pending_offset))
else {
break;
};
let Some((trimmed_offset, bytes)) =
trim_stream_data_to_offset(pending_offset, bytes, *expected)
else {
continue;
};
if trimmed_offset > *expected {
insert_pending_stream_data(
recv_pending.entry(stream_id).or_default(),
trimmed_offset,
bytes,
);
break;
}
consumed = consumed.saturating_add(bytes.len());
*expected = expected.saturating_add(bytes.len() as u64);
chunks.push(bytes);
}
if recv_pending.get(&stream_id).is_some_and(BTreeMap::is_empty) {
recv_pending.remove(&stream_id);
}
(chunks, consumed, *expected)
}
pub fn stream_data_chunk_limit(receive_window: usize) -> usize {
MAX_STREAM_DATA_BYTES.min(receive_window.max(1))
}
@@ -1327,6 +1378,79 @@ mod tests {
assert_eq!(oversized_current.received_offset, 0);
}
#[test]
fn overlapping_out_of_order_data_trims_deliverable_suffixes() {
let mut mux = StreamMux::new(TransportConfig {
initial_window: 16,
..TransportConfig::default()
});
mux.accept_open(StreamOpen {
stream_id: 3,
target_host: "@dosh-test".to_string(),
target_port: 0,
})
.unwrap();
assert!(
mux.handle_data(StreamData {
stream_id: 3,
offset: 5,
bytes: b"FGHIJ".to_vec(),
})
.unwrap()
.unwrap()
.chunks
.is_empty()
);
assert!(
mux.handle_data(StreamData {
stream_id: 3,
offset: 3,
bytes: b"DEFGH".to_vec(),
})
.unwrap()
.unwrap()
.chunks
.is_empty()
);
let delivered = mux
.handle_data(StreamData {
stream_id: 3,
offset: 0,
bytes: b"ABC".to_vec(),
})
.unwrap()
.unwrap();
assert_eq!(
delivered.chunks.concat(),
b"ABCDEFGHIJ",
"overlapping pending suffix should be trimmed and delivered"
);
assert_eq!(delivered.consumed, 10);
assert_eq!(delivered.received_offset, 10);
assert!(!mux.recv_pending.contains_key(&3));
}
#[test]
fn same_offset_pending_data_keeps_longest_range() {
let mut next = HashMap::from([(1, 0)]);
let mut pending = HashMap::new();
let first = accept_stream_data_chunks(&mut next, &mut pending, 1, 3, b"DE".to_vec(), 16);
assert!(first.0.is_empty());
let longer = accept_stream_data_chunks(&mut next, &mut pending, 1, 3, b"DEFG".to_vec(), 16);
assert!(longer.0.is_empty());
let delivered =
accept_stream_data_chunks(&mut next, &mut pending, 1, 0, b"ABC".to_vec(), 16);
assert_eq!(delivered.0.concat(), b"ABCDEFG");
assert_eq!(delivered.1, 7);
assert_eq!(delivered.2, 7);
assert!(pending.is_empty());
}
#[test]
fn receive_window_helper_rejects_overflowing_ranges() {
assert!(stream_data_within_receive_window(0, 4, 1, 5));