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
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:
+6
-40
@@ -8124,48 +8124,14 @@ fn accept_stream_data(
|
|||||||
stream_recv_pending: &mut HashMap<u64, BTreeMap<u64, Vec<u8>>>,
|
stream_recv_pending: &mut HashMap<u64, BTreeMap<u64, Vec<u8>>>,
|
||||||
data: StreamData,
|
data: StreamData,
|
||||||
) -> (Vec<Vec<u8>>, usize, u64) {
|
) -> (Vec<Vec<u8>>, usize, u64) {
|
||||||
let stream_id = data.stream_id;
|
dosh::transport::accept_stream_data_chunks(
|
||||||
let expected = stream_next_recv_offset.entry(stream_id).or_insert(0);
|
stream_next_recv_offset,
|
||||||
if !dosh::transport::stream_data_within_receive_window(
|
stream_recv_pending,
|
||||||
*expected,
|
data.stream_id,
|
||||||
data.offset,
|
data.offset,
|
||||||
data.bytes.len(),
|
data.bytes,
|
||||||
STREAM_INITIAL_WINDOW,
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|||||||
+6
-43
@@ -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) {
|
fn accept_stream_data(client: &mut ClientState, data: StreamData) -> (Vec<Vec<u8>>, usize, u64) {
|
||||||
let stream_id = data.stream_id;
|
dosh::transport::accept_stream_data_chunks(
|
||||||
let expected = client.stream_next_recv_offset.entry(stream_id).or_insert(0);
|
&mut client.stream_next_recv_offset,
|
||||||
if !dosh::transport::stream_data_within_receive_window(
|
&mut client.stream_recv_pending,
|
||||||
*expected,
|
data.stream_id,
|
||||||
data.offset,
|
data.offset,
|
||||||
data.bytes.len(),
|
data.bytes,
|
||||||
STREAM_INITIAL_WINDOW,
|
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) {
|
fn cleanup_client_stream(client: &mut ClientState, stream_id: u64) {
|
||||||
|
|||||||
+165
-41
@@ -654,49 +654,14 @@ impl StreamMux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn accept_data(&mut self, data: StreamData) -> (Vec<Vec<u8>>, usize, u64) {
|
fn accept_data(&mut self, data: StreamData) -> (Vec<Vec<u8>>, usize, u64) {
|
||||||
let stream_id = data.stream_id;
|
accept_stream_data_chunks(
|
||||||
let expected = self.next_recv_offset.entry(stream_id).or_insert(0);
|
&mut self.next_recv_offset,
|
||||||
if !stream_data_within_receive_window(
|
&mut self.recv_pending,
|
||||||
*expected,
|
data.stream_id,
|
||||||
data.offset,
|
data.offset,
|
||||||
data.bytes.len(),
|
data.bytes,
|
||||||
self.config.initial_window,
|
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 {
|
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)
|
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 {
|
pub fn stream_data_chunk_limit(receive_window: usize) -> usize {
|
||||||
MAX_STREAM_DATA_BYTES.min(receive_window.max(1))
|
MAX_STREAM_DATA_BYTES.min(receive_window.max(1))
|
||||||
}
|
}
|
||||||
@@ -1327,6 +1378,79 @@ mod tests {
|
|||||||
assert_eq!(oversized_current.received_offset, 0);
|
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]
|
#[test]
|
||||||
fn receive_window_helper_rejects_overflowing_ranges() {
|
fn receive_window_helper_rejects_overflowing_ranges() {
|
||||||
assert!(stream_data_within_receive_window(0, 4, 1, 5));
|
assert!(stream_data_within_receive_window(0, 4, 1, 5));
|
||||||
|
|||||||
Reference in New Issue
Block a user