use std::{
cmp::min,
collections::VecDeque,
fmt::{self, Display, Formatter},
time::Instant,
};
use neqo_common::{Header, qdebug, qerror, qlog::Qlog, qtrace}; use neqo_transport::{Connection, Error as TransportError, StreamId}; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
#[derive(Debug)] pubstruct Encoder {
table: HeaderTable,
max_table_size: u64,
max_entries: u64,
instruction_reader: DecoderInstructionReader,
local_stream: LocalStreamState,
max_blocked_streams: u16, // Remember header blocks that are referring to dynamic table. // There can be multiple header blocks in one stream, headers, trailer, push stream request, // etc. This HashMap maps a stream ID to a list of header blocks. Each header block is a // list of referenced dynamic table entries.
unacked_header_blocks: HashMap<StreamId, VecDeque<HashSet<u64>>>,
blocked_stream_cnt: u16,
use_huffman: bool,
next_capacity: Option<u64>,
stats: Stats,
}
/// This function is use for setting encoders table max capacity. The value is received as /// a `SETTINGS_QPACK_MAX_TABLE_CAPACITY` setting parameter. /// /// # Errors /// /// `EncoderStream` if value is too big. /// `ChangeCapacity` if table capacity cannot be reduced. pubfn set_max_capacity(&mutself, cap: u64) -> Res<()> { if cap > (1 << 30) - 1 { return Err(Error::EncoderStream);
}
if cap == self.table.capacity() { return Ok(());
}
qdebug!( "[{self}] Set max capacity to new capacity:{cap} old:{} max_table_size={}", self.table.capacity(), self.max_table_size,
);
let new_cap = min(self.max_table_size, cap); // we also set our table to the max allowed. self.change_capacity(new_cap);
Ok(())
}
/// This function is use for setting encoders max blocked streams. The value is received as /// a `SETTINGS_QPACK_BLOCKED_STREAMS` setting parameter. /// /// # Errors /// /// `EncoderStream` if value is too big. pubfn set_max_blocked_streams(&mutself, blocked_streams: u64) -> Res<()> { self.max_blocked_streams = u16::try_from(blocked_streams).or(Err(Error::EncoderStream))?;
Ok(())
}
/// Reads decoder instructions. /// /// # Errors /// /// May return: `ClosedCriticalStream` if stream has been closed or `DecoderStream` /// in case of any other transport error. pubfn receive(&mutself, conn: &mut Connection, stream_id: StreamId, now: Instant) -> Res<()> { self.read_instructions(conn, stream_id, now)
.map_err(|e| map_error(&e))
}
fn header_ack(&mutself, stream_id: StreamId) { self.stats.header_acks_recv += 1; letmut new_acked = self.table.get_acked_inserts_cnt(); iflet Some(hb_list) = self.unacked_header_blocks.get_mut(&stream_id) { iflet Some(ref_list) = hb_list.pop_back() { #[expect(
clippy::iter_over_hash_type,
reason = "OK to loop over unACKed blocks in an undefined order."
)] for iter in ref_list { self.table.remove_ref(iter); if iter >= new_acked {
new_acked = iter + 1;
}
}
} else {
debug_assert!(false, "We should have at least one header block");
} if hb_list.is_empty() { self.unacked_header_blocks.remove(&stream_id);
}
} if new_acked > self.table.get_acked_inserts_cnt() { self.insert_count_instruction(new_acked - self.table.get_acked_inserts_cnt())
.expect("This should neve happen");
}
}
fn stream_cancellation(&mutself, stream_id: StreamId) { self.stats.stream_cancelled_recv += 1; letmut was_blocker = false; iflet Some(mut hb_list) = self.unacked_header_blocks.remove(&stream_id) {
debug_assert!(!hb_list.is_empty()); whilelet Some(ref_list) = hb_list.pop_front() { #[expect(
clippy::iter_over_hash_type,
reason = "OK to loop over unACKed blocks in an undefined order."
)] for iter in ref_list { self.table.remove_ref(iter);
was_blocker = was_blocker || (iter >= self.table.get_acked_inserts_cnt());
}
}
} if was_blocker {
debug_assert!(self.blocked_stream_cnt > 0); self.blocked_stream_cnt -= 1;
}
}
/// Inserts a new entry into a table and sends the corresponding instruction to a peer. An entry /// is added only if it is possible to send the corresponding instruction immediately, i.e. /// the encoder stream is not blocked by the flow control (or stream internal buffer(this is /// very unlikely)). /// /// # Errors /// /// `EncoderStreamBlocked` if the encoder stream is blocked by the flow control. /// `DynamicTableFull` if the dynamic table does not have enough space for the entry. /// The function can return transport errors: `InvalidStreamId`, `InvalidInput` and /// `FinalSizeError`. /// /// # Panics /// /// When the insertion fails (it should not). pubfn send_and_insert(
&mutself,
conn: &mut Connection,
name: &[u8],
value: &[u8],
) -> Res<u64> {
qdebug!("[{self}] insert {name:?} {value:?}");
let entry_size = name.len() + value.len() + ADDITIONAL_TABLE_ENTRY_SIZE;
if !self.table.insert_possible(entry_size) { return Err(Error::DynamicTableFull);
}
let stream_id = self.local_stream.stream_id().ok_or(Error::Internal)?;
let sent = conn
.stream_send_atomic(stream_id, buf.as_ref())
.map_err(|e| map_stream_send_atomic_error(&e))?; if !sent { return Err(Error::EncoderStreamBlocked);
}
fn maybe_send_change_capacity(
&mutself,
conn: &mut Connection,
stream_id: StreamId,
) -> Res<()> { iflet Some(cap) = self.next_capacity { // Check if it is possible to reduce the capacity, e.g. if enough space can be made free // for the reduction. if cap < self.table.capacity() && !self.table.can_evict_to(cap) { return Err(Error::DynamicTableFull);
} letmut buf = neqo_common::Encoder::default();
EncoderInstruction::Capacity { value: cap }.marshal(&mut buf, self.use_huffman); if !conn.stream_send_atomic(stream_id, buf.as_ref())? { return Err(Error::EncoderStreamBlocked);
} ifself.table.set_capacity(cap).is_err() {
debug_assert!( false, "can_evict_to should have checked and make sure this operation is possible"
); return Err(Error::Internal);
} self.max_entries = cap / 32; self.next_capacity = None;
}
Ok(())
}
/// Sends any qpack encoder instructions. /// /// # Errors /// /// returns `EncoderStream` in case of an error. pubfn send_encoder_updates(&mutself, conn: &mut Connection) -> Res<()> { matchself.local_stream {
LocalStreamState::NoStream => {
qerror!("Send call but there is no stream yet");
Ok(())
}
LocalStreamState::Uninitialized(stream_id) => { letmut buf = neqo_common::Encoder::default();
buf.encode_varint(QPACK_UNI_STREAM_TYPE_ENCODER); if !conn.stream_send_atomic(stream_id, buf.as_ref())? { return Err(Error::EncoderStreamBlocked);
} self.local_stream = LocalStreamState::Initialized(stream_id); self.maybe_send_change_capacity(conn, stream_id)
}
LocalStreamState::Initialized(stream_id) => { self.maybe_send_change_capacity(conn, stream_id)
}
}
}
/// Encodes headers /// /// # Errors /// /// `ClosedCriticalStream` if the encoder stream is closed. /// `InternalError` if an unexpected error occurred. /// /// # Panics /// /// If there is a programming error. pubfn encode_header_block(
&mutself,
conn: &mut Connection,
h: &[Header],
stream_id: StreamId,
) -> HeaderEncoder {
qdebug!("[{self}] encoding headers");
// Try to send capacity instructions if present. // This code doesn't try to deal with errors, it just tries // to write to the encoder stream AND if it can't uses // literal instructions. // The errors can be: // 1) `EncoderStreamBlocked` - this is an error that can occur. // 2) `InternalError` - this is unexpected error. // 3) `ClosedCriticalStream` - this is error that should close the HTTP/3 session. // The last 2 errors are ignored here and will be picked up // by the main loop. letmut encoder_blocked = self.send_encoder_updates(conn).is_err();
let stream_is_blocker = self.is_stream_blocker(stream_id); let can_block = self.blocked_stream_cnt < self.max_blocked_streams || stream_is_blocker;
letmut ref_entries = HashSet::default();
for iter in h { let name = iter.name().as_bytes().to_vec(); let value = iter.value();
qtrace!("encoding {name:x?} {value:x?}");
iflet Some(LookupResult {
index,
static_table,
value_matches,
}) = self.table.lookup(&name, value, can_block)
{
qtrace!( "[{self}] found a {} entry, value-match={value_matches}", if static_table { "static" } else { "dynamic" }
); if value_matches { if static_table {
encoded_h.encode_indexed_static(index);
} else {
encoded_h.encode_indexed_dynamic(index);
}
} else {
encoded_h.encode_literal_with_name_ref(static_table, index, value);
} if !static_table && ref_entries.insert(index) { self.table.add_ref(index);
}
} elseif can_block && !encoder_blocked { // Insert using an InsertWithNameLiteral instruction. This entry name does not match // any name in the tables therefore we cannot use any other // instruction. iflet Ok(index) = self.send_and_insert(conn, &name, value) {
encoded_h.encode_indexed_dynamic(index);
ref_entries.insert(index); self.table.add_ref(index);
} else { // This code doesn't try to deal with errors, it just tries // to write to the encoder stream AND if it can't uses // literal instructions. // The errors can be: // 1) `EncoderStreamBlocked` - this is an error that can occur. // 2) `DynamicTableFull` - this is an error that can occur. // 3) `InternalError` - this is unexpected error. // 4) `ClosedCriticalStream` - this is error that should close the HTTP/3 // session. // The last 2 errors are ignored here and will be picked up // by the main loop. // As soon as one of the instructions cannot be written or the table is full, do // not try again.
encoder_blocked = true;
encoded_h.encode_literal_with_name_literal(&name, value);
}
} else {
encoded_h.encode_literal_with_name_literal(&name, value);
}
}
encoded_h.encode_header_block_prefix();
if !stream_is_blocker { // The streams was not a blocker, check if the stream is a blocker now. iflet Some(max_ref) = ref_entries.iter().max()
&& *max_ref >= self.table.get_acked_inserts_cnt()
{
debug_assert!(self.blocked_stream_cnt <= self.max_blocked_streams); self.blocked_stream_cnt += 1;
}
}
impl TestEncoder { pubfn change_capacity(&mutself, capacity: u64) -> Res<()> { self.encoder.set_max_capacity(capacity)?; // We will try to really change the table only when we send the change capacity // instruction. self.encoder.send_encoder_updates(&mutself.conn)
}
pubfn insert(&mutself, header: &[u8], value: &[u8], inst: &[u8]) { let res = self.encoder.send_and_insert(&mutself.conn, header, value);
assert!(res.is_ok()); self.send_instructions(inst);
}
// create a stream let recv_stream_id = peer_conn.stream_create(StreamType::UniDi).unwrap(); let send_stream_id = conn.stream_create(StreamType::UniDi).unwrap();
// test insert_with_name_literal which fails because there is not enough space in the table #[test] fn insert_with_name_literal_1() { letmut encoder = connect(false);
// insert "content-length: 1234 let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_1);
assert_eq!(Error::DynamicTableFull, res.unwrap_err());
encoder.send_instructions(&[0x02]);
}
// test the change capacity instruction.
encoder.send_instructions(CAP_INSTRUCTION_200);
for t in &test_cases { let buf = encoder
.encoder
.encode_header_block(&mut encoder.conn, &t.headers, STREAM_1);
assert_eq!(buf.as_ref(), t.header_block);
encoder.send_instructions(t.encoder_inst);
}
}
// test the change capacity instruction.
encoder.send_instructions(CAP_INSTRUCTION_200);
for t in &test_cases { let buf = encoder
.encoder
.encode_header_block(&mut encoder.conn, &t.headers, STREAM_1);
assert_eq!(buf.as_ref(), t.header_block);
encoder.send_instructions(t.encoder_inst);
}
}
// Test inserts block on waiting for an insert count increment. #[test] fn insertion_blocked_on_insert_count_feedback() { letmut encoder = connect(false);
encoder.encoder.set_max_capacity(60).unwrap();
// test the change capacity instruction.
encoder.send_instructions(CAP_INSTRUCTION_60);
// insert "content-length: 1234 let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_1);
assert!(res.is_ok());
encoder.send_instructions(HEADER_CONTENT_LENGTH_VALUE_1_NAME_LITERAL);
// insert "content-length: 12345 which will fail because the entry in the table cannot be // evicted. let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_2);
assert!(res.is_err());
encoder.send_instructions(&[]);
// receive an insert count increment.
recv_instruction(&mut encoder, &[0x01], now());
// insert "content-length: 12345 again it will succeed. let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_2);
assert!(res.is_ok());
encoder.send_instructions(HEADER_CONTENT_LENGTH_VALUE_2_NAME_LITERAL);
}
// Test inserts block on waiting for ACKs // test the table insertion is blocked: // 0 - waiting for a header ack // 2 - waiting for a stream cancel. fn test_insertion_blocked_on_waiting_for_header_ack_or_stream_cancel(wait: u8) { letmut encoder = connect(false);
assert!(encoder.encoder.set_max_capacity(60).is_ok()); // test the change capacity instruction.
encoder.send_instructions(CAP_INSTRUCTION_60);
// insert "content-length: 1234 let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_1);
assert!(res.is_ok());
encoder.send_instructions(HEADER_CONTENT_LENGTH_VALUE_1_NAME_LITERAL);
// receive an insert count increment.
recv_instruction(&mut encoder, &[0x01], now());
// send a header block let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("content-length", "1234")],
STREAM_1,
);
assert_eq!(buf.as_ref(), ENCODE_INDEXED_REF_DYNAMIC);
encoder.send_instructions(&[]);
// insert "content-length: 12345 which will fail because the entry in the table cannot be // evicted let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_2);
assert!(res.is_err());
encoder.send_instructions(&[]);
if wait == 0 { // receive a header_ack.
recv_instruction(&mut encoder, HEADER_ACK_STREAM_ID_1, now());
} else { // receive a stream canceled
recv_instruction(&mut encoder, STREAM_CANCELED_ID_1, now());
}
// insert "content-length: 12345 again it will succeed. let res =
encoder
.encoder
.send_and_insert(&mut encoder.conn, HEADER_CONTENT_LENGTH, VALUE_2);
assert!(res.is_ok());
encoder.send_instructions(HEADER_CONTENT_LENGTH_VALUE_2_NAME_LITERAL);
}
// The next one will not use the dynamic entry because it is exceeding the // max_blocked_streams limit. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("content-length", "1234")],
StreamId::new(2),
);
assert_is_index_to_static_name_only(&buf);
// another header block to already blocked stream can still use the entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("content-length", "1234")],
STREAM_1,
);
assert_is_index_to_dynamic(&buf);
// send a header block, it refers to unacked entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("content-length", "1234")],
STREAM_1,
);
assert_is_index_to_dynamic(&buf);
// encode another header block for the same stream that will refer to the second entry // in the dynamic table. // This should work because the stream is already a blocked stream // send a header block, it refers to unacked entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("content-length", "12345")],
STREAM_1,
);
assert_is_index_to_dynamic(&buf);
}
// send a header block, that creates an new entry and refers to it. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// The next one will not create a new entry because the encoder is on max_blocked_streams // limit. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name2", "value2")],
STREAM_2,
);
assert_is_literal_value_literal_name(&buf);
// another header block to already blocked stream can still create a new entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name2", "value2")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// send a header block, that creates an new entry and refers to it. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// another header block to already blocked stream can still create a new entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name2", "value2")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// send a header block, that creates an new entry and refers to it. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// another header block to already blocked stream can still create a new entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic(&buf);
// send a header block, that creates an new entry and refers to it. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// header block for the next stream will create an new entry as well. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name2", "value2")],
STREAM_2,
);
assert_is_index_to_dynamic_post(&buf);
// send a header block, that creates an new entry and refers to it. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// header block for the next stream will create an new entry as well. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_2,
);
assert_is_index_to_dynamic(&buf);
// receive a stream cancel for the first stream. // This will remove the first stream as blocking but it will not mark the instruction as // acked. and the second steam will still be blocking.
recv_instruction(&mut encoder, STREAM_CANCELED_ID_1, now());
// The stream is not blocking anymore because header ack also ACKs the instruction.
assert_eq!(encoder.encoder.blocked_stream_cnt(), 1);
}
// send a header block, that creates an new entry and refers to it. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_1,
);
assert_is_index_to_dynamic_post(&buf);
// header block for the next stream will refer to the same entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("name1", "value1")],
STREAM_2,
);
assert_is_index_to_dynamic(&buf);
// stream 1 is block on entries 1 and 2; stream 2 is block only on 1. // receive an Insert Count Increment for the first entry. // After that only stream 1 will be blocking.
recv_instruction(&mut encoder, &[0x01], now());
// send a header block, it refers to unacked entry. let buf = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[Header::new("content-length", "1234")],
STREAM_1,
);
assert_is_index_to_dynamic(&buf);
// trying to evict the entry will failed. The stream is still referring to it and // entry is not acked.
assert!(encoder.change_capacity(10).is_err());
// receive a header_ack for the header block. This will also ack the instruction.
recv_instruction(&mut encoder, HEADER_ACK_STREAM_ID_1, now());
// now entry can be evicted.
assert!(encoder.change_capacity(10).is_ok());
}
// change capacity to 1000 and max_block streams to 20.
encoder.encoder.set_max_blocked_streams(20).unwrap();
assert!(encoder.encoder.set_max_capacity(1000).is_ok());
encoder.send_instructions(CAP_INSTRUCTION_1000);
// Encode a header block with 2 headers. The first header will be added to the dynamic // table. The second will not be added to the dynamic table, because the // corresponding instruction cannot be written immediately due to the flow control // limit. let buf1 = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[
Header::new("something", "1234"),
Header::new("something2", "12345678910"),
],
STREAM_1,
);
// Assert that the first header is encoded as an index to the dynamic table (a post form).
assert!(buf1.len() > 3);
assert_eq!(buf1[2], 0x10); // Assert that the second header is encoded as a literal with a name literal
assert_eq!(buf1[3] & 0xf0, 0x20);
// Try to encode another header block. Here both headers will be encoded as a literal with a // name literal let buf2 = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[
Header::new("something3", "1234"),
Header::new("something4", "12345678910"),
],
STREAM_2,
);
assert_eq!(buf2[2] & 0xf0, 0x20);
// Ensure that we have sent only one instruction for (String::from("something", "1234"))
encoder.send_instructions(ONE_INSTRUCTION_1);
// exchange a flow control update. let out = encoder.peer_conn.process_output(now());
drop(encoder.conn.process(out.dgram(), now()));
// Try writing a new header block. Now, headers will be added to the dynamic table again, // because instructions can be sent. let buf3 = encoder.encoder.encode_header_block(
&mut encoder.conn,
&[
Header::new("something5", "1234"),
Header::new("something6", "12345678910"),
],
StreamId::new(3),
); // Assert that the first header is encoded as an index to the dynamic table (a post form).
assert!(buf3.len() > 3);
assert_eq!(buf3[2], 0x10); // Assert that the second header is encoded as a literal with a name literal
assert_eq!(buf3[3] & 0xf0, 0x20);
// Asset that one instruction has been sent
encoder.send_instructions(ONE_INSTRUCTION_2);
}
encoder
.encoder
.send_encoder_updates(&mut encoder.conn)
.unwrap(); let out = encoder.conn.process_output(now());
drop(encoder.peer_conn.process(out.dgram(), now())); // receive an insert count increment.
recv_instruction(&mut encoder, &[0x01], now());
// The first header will use the table entry and the second will use the literal // encoding because the first entry is referred to and cannot be evicted.
assert_eq!(
encoder
.encoder
.encode_header_block(
&mut encoder.conn,
&[
Header::new("something5", "1234"),
Header::new("something6", "1234"),
],
StreamId::new(3),
)
.to_vec(),
&[ 0x02, 0x00, 0x80, 0x27, 0x03, 0x73, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x36, 0x04, 0x31, 0x32, 0x33, 0x34
]
); // Also check that there is no new instruction send by the encoder.
assert!(encoder.conn.process_output(now()).dgram().is_none());
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.