Skip to main content

net_wire/qpack/stream/encoder/
apply.rs

1//! Application of parsed RFC 9204 encoder-stream instructions.
2
3use core::fmt;
4
5use super::{
6    QpackEncoderInstruction, QpackEncoderInstructions, QpackEncoderInstructionsParseError,
7};
8use crate::qpack::huffman::{QpackHuffmanDecodeError, QpackHuffmanDecoder};
9use crate::qpack::string::QpackStringLiteral;
10use crate::qpack::table::{
11    QPACK_STATIC_TABLE_LEN, QpackDynamicTable, QpackDynamicTableError, QpackStaticTable,
12};
13
14/// Applies one parsed QPACK encoder-stream instruction to a dynamic table.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct QpackEncoderInstructionApplier {
17    maximum_dynamic_table_capacity: u64,
18}
19
20impl QpackEncoderInstructionApplier {
21    /// Creates an applier with the locally advertised maximum dynamic-table capacity.
22    pub const fn new(maximum_dynamic_table_capacity: u64) -> Self {
23        Self {
24            maximum_dynamic_table_capacity,
25        }
26    }
27
28    /// Returns the locally advertised maximum permitted dynamic-table capacity.
29    pub const fn maximum_dynamic_table_capacity(self) -> u64 {
30        self.maximum_dynamic_table_capacity
31    }
32
33    /// Applies one instruction using caller-owned scratch storage for decoded or copied bytes.
34    ///
35    /// Scratch is unchanged when preflight fails. Once staging starts, a later rejected eviction
36    /// policy may leave scratch clobbered, while the table remains unchanged on every error.
37    /// Effects observed by `evictable` cannot be rolled back.
38    pub fn apply<F>(
39        &self,
40        table: &mut QpackDynamicTable<'_, '_>,
41        instruction: QpackEncoderInstruction<'_>,
42        scratch: &mut [u8],
43        evictable: F,
44    ) -> Result<QpackEncoderInstructionApplyOutcome, QpackEncoderInstructionApplyError>
45    where
46        F: FnMut(u64) -> bool,
47    {
48        match instruction {
49            QpackEncoderInstruction::SetDynamicTableCapacity(instruction) => {
50                let requested = instruction.capacity().value();
51                if requested > self.maximum_dynamic_table_capacity {
52                    return Err(QpackEncoderInstructionApplyError::CapacityExceedsMaximum {
53                        requested,
54                        maximum: self.maximum_dynamic_table_capacity,
55                    });
56                }
57                let capacity = usize::try_from(requested).map_err(|_| {
58                    QpackEncoderInstructionApplyError::CapacityNotRepresentable { value: requested }
59                })?;
60                table
61                    .set_capacity(capacity, evictable)
62                    .map_err(QpackEncoderInstructionApplyError::CapacityChange)?;
63                Ok(QpackEncoderInstructionApplyOutcome::CapacityUpdated { capacity })
64            }
65            QpackEncoderInstruction::InsertWithNameReference(instruction) => {
66                let value = instruction.value();
67                if instruction.is_static() {
68                    let index = instruction.name_index().value();
69                    if index >= QPACK_STATIC_TABLE_LEN as u64 {
70                        return Err(
71                            QpackEncoderInstructionApplyError::StaticNameIndexOutOfRange { index },
72                        );
73                    }
74                    let name = QpackStaticTable::get(index as usize)
75                        .ok_or(
76                            QpackEncoderInstructionApplyError::StaticNameIndexOutOfRange { index },
77                        )?
78                        .name();
79                    let value_len = decoded_len(value, false)?;
80                    let required = if value.is_huffman() { value_len } else { 0 };
81                    preflight_insert(table, name.len(), value_len, required, scratch.len())?;
82                    let value = if value.is_huffman() {
83                        decode(value, &mut scratch[..value_len], false)?
84                    } else {
85                        value.encoded_payload()
86                    };
87                    insert(table, name, value, evictable)
88                } else {
89                    let relative = instruction.name_index().value();
90                    let name_len = table
91                        .get_encoder_relative(relative)
92                        .map_err(QpackEncoderInstructionApplyError::DynamicNameReference)?
93                        .name()
94                        .len();
95                    let value_len = decoded_len(value, false)?;
96                    let required =
97                        checked_sum(name_len, if value.is_huffman() { value_len } else { 0 })?;
98                    preflight_insert(table, name_len, value_len, required, scratch.len())?;
99                    let (name_destination, value_destination) =
100                        scratch[..required].split_at_mut(name_len);
101                    let source = table
102                        .get_encoder_relative(relative)
103                        .map_err(QpackEncoderInstructionApplyError::DynamicNameReference)?;
104                    name_destination.copy_from_slice(source.name());
105                    let value = if value.is_huffman() {
106                        decode(value, value_destination, false)?
107                    } else {
108                        value.encoded_payload()
109                    };
110                    insert(table, name_destination, value, evictable)
111                }
112            }
113            QpackEncoderInstruction::InsertWithLiteralName(instruction) => {
114                let name_literal = instruction.name();
115                let value_literal = instruction.value();
116                let name_len = decoded_len(name_literal, true)?;
117                let value_len = decoded_len(value_literal, false)?;
118                let name_scratch = if name_literal.is_huffman() {
119                    name_len
120                } else {
121                    0
122                };
123                let value_scratch = if value_literal.is_huffman() {
124                    value_len
125                } else {
126                    0
127                };
128                let required = checked_sum(name_scratch, value_scratch)?;
129                preflight_insert(table, name_len, value_len, required, scratch.len())?;
130                let (name_destination, value_destination) =
131                    scratch[..required].split_at_mut(name_scratch);
132                let name = if name_literal.is_huffman() {
133                    decode(name_literal, name_destination, true)?
134                } else {
135                    name_literal.encoded_payload()
136                };
137                let value = if value_literal.is_huffman() {
138                    decode(value_literal, value_destination, false)?
139                } else {
140                    value_literal.encoded_payload()
141                };
142                insert(table, name, value, evictable)
143            }
144            QpackEncoderInstruction::Duplicate(instruction) => {
145                let relative = instruction.index().value();
146                let source = table
147                    .get_encoder_relative(relative)
148                    .map_err(QpackEncoderInstructionApplyError::DuplicateReference)?;
149                let name_len = source.name().len();
150                let value_len = source.value().len();
151                let required = checked_sum(name_len, value_len)?;
152                preflight_insert(table, name_len, value_len, required, scratch.len())?;
153                let source = table
154                    .get_encoder_relative(relative)
155                    .map_err(QpackEncoderInstructionApplyError::DuplicateReference)?;
156                let (name, value) = scratch[..required].split_at_mut(name_len);
157                name.copy_from_slice(source.name());
158                value.copy_from_slice(source.value());
159                insert(table, name, value, evictable)
160            }
161        }
162    }
163
164    /// Validates and applies a complete unframed encoder-stream instruction sequence.
165    ///
166    /// The complete sequence is validated before any instruction is applied. On an application
167    /// failure, earlier successful instructions remain applied and later instructions are not
168    /// attempted. Scratch and `evictable` retain the per-instruction effects documented by
169    /// [`Self::apply`].
170    pub fn apply_sequence<F>(
171        &self,
172        table: &mut QpackDynamicTable<'_, '_>,
173        bytes: &[u8],
174        scratch: &mut [u8],
175        mut evictable: F,
176    ) -> Result<(), QpackEncoderInstructionsApplyError>
177    where
178        F: FnMut(u64) -> bool,
179    {
180        let instructions = QpackEncoderInstructions::parse(bytes)
181            .map_err(QpackEncoderInstructionsApplyError::Parse)?;
182        let mut offset = 0usize;
183
184        for instruction in instructions.iter() {
185            let instruction = instruction.map_err(QpackEncoderInstructionsApplyError::Parse)?;
186            self.apply(table, instruction, scratch, &mut evictable)
187                .map_err(|error| QpackEncoderInstructionsApplyError::Apply { offset, error })?;
188            offset += instruction.as_bytes().len();
189        }
190
191        Ok(())
192    }
193}
194
195/// The successful result of applying one encoder-stream instruction.
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub enum QpackEncoderInstructionApplyOutcome {
198    /// The dynamic-table capacity changed.
199    CapacityUpdated {
200        /// The new RFC dynamic-table capacity.
201        capacity: usize,
202    },
203    /// A field was inserted into the dynamic table.
204    Inserted {
205        /// The new entry's zero-based absolute index.
206        absolute_index: u64,
207    },
208}
209
210/// Failure to apply one QPACK encoder-stream instruction.
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub enum QpackEncoderInstructionApplyError {
213    /// A capacity instruction exceeds the configured locally advertised maximum.
214    CapacityExceedsMaximum {
215        /// The requested capacity.
216        requested: u64,
217        /// The configured maximum capacity.
218        maximum: u64,
219    },
220    /// A capacity cannot be represented by this target's `usize`.
221    CapacityNotRepresentable {
222        /// The unrepresentable capacity value.
223        value: u64,
224    },
225    /// Updating dynamic-table capacity failed.
226    CapacityChange(QpackDynamicTableError),
227    /// A static name-reference index is outside the static table.
228    StaticNameIndexOutOfRange {
229        /// The invalid static-table index.
230        index: u64,
231    },
232    /// Resolving a dynamic name reference failed.
233    DynamicNameReference(QpackDynamicTableError),
234    /// Resolving a Duplicate reference failed.
235    DuplicateReference(QpackDynamicTableError),
236    /// Decoding a literal name failed.
237    LiteralNameHuffman(QpackHuffmanDecodeError),
238    /// Decoding a value failed.
239    ValueHuffman(QpackHuffmanDecodeError),
240    /// Computing the required scratch length overflowed `usize`.
241    ScratchLengthOverflow,
242    /// Caller scratch storage cannot contain all required staged bytes.
243    ScratchTooShort {
244        /// Staging bytes required.
245        required: usize,
246        /// Caller bytes available.
247        available: usize,
248    },
249    /// Inserting the preflighted field failed.
250    Insert(QpackDynamicTableError),
251}
252
253impl QpackEncoderInstructionApplyError {
254    /// Returns whether this failure is an RFC QPACK encoder-stream error.
255    pub const fn is_encoder_stream_error(self) -> bool {
256        match self {
257            Self::CapacityExceedsMaximum { .. }
258            | Self::StaticNameIndexOutOfRange { .. }
259            | Self::DynamicNameReference(_)
260            | Self::DuplicateReference(_) => true,
261            Self::LiteralNameHuffman(
262                QpackHuffmanDecodeError::EosSymbol
263                | QpackHuffmanDecodeError::InvalidHuffmanCode
264                | QpackHuffmanDecodeError::InvalidPadding,
265            )
266            | Self::ValueHuffman(
267                QpackHuffmanDecodeError::EosSymbol
268                | QpackHuffmanDecodeError::InvalidHuffmanCode
269                | QpackHuffmanDecodeError::InvalidPadding,
270            ) => true,
271            Self::CapacityChange(QpackDynamicTableError::EvictionBlocked { .. }) => true,
272            Self::Insert(QpackDynamicTableError::EntryLargerThanCapacity { .. })
273            | Self::Insert(QpackDynamicTableError::EvictionBlocked { .. }) => true,
274            Self::CapacityNotRepresentable { .. }
275            | Self::CapacityChange(_)
276            | Self::LiteralNameHuffman(_)
277            | Self::ValueHuffman(_)
278            | Self::ScratchLengthOverflow
279            | Self::ScratchTooShort { .. }
280            | Self::Insert(_) => false,
281        }
282    }
283}
284
285impl fmt::Display for QpackEncoderInstructionApplyError {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        match self {
288            Self::CapacityExceedsMaximum { requested, maximum } => {
289                write!(f, "QPACK capacity {requested} exceeds maximum {maximum}")
290            }
291            Self::CapacityNotRepresentable { value } => {
292                write!(f, "QPACK capacity cannot be represented: {value}")
293            }
294            Self::CapacityChange(error) => write!(f, "QPACK capacity change failed: {error}"),
295            Self::StaticNameIndexOutOfRange { index } => {
296                write!(f, "QPACK static name index is out of range: {index}")
297            }
298            Self::DynamicNameReference(error) => {
299                write!(f, "QPACK dynamic name reference failed: {error}")
300            }
301            Self::DuplicateReference(error) => {
302                write!(f, "QPACK Duplicate reference failed: {error}")
303            }
304            Self::LiteralNameHuffman(error) => {
305                write!(f, "QPACK literal name Huffman decode failed: {error}")
306            }
307            Self::ValueHuffman(error) => write!(f, "QPACK value Huffman decode failed: {error}"),
308            Self::ScratchLengthOverflow => f.write_str("QPACK scratch length overflows usize"),
309            Self::ScratchTooShort {
310                required,
311                available,
312            } => write!(
313                f,
314                "QPACK scratch is too short: need {required} bytes, have {available}"
315            ),
316            Self::Insert(error) => write!(f, "QPACK insertion failed: {error}"),
317        }
318    }
319}
320
321impl core::error::Error for QpackEncoderInstructionApplyError {}
322
323/// Failure to validate or apply a QPACK encoder-stream instruction sequence.
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
325pub enum QpackEncoderInstructionsApplyError {
326    /// The complete sequence is malformed before application begins.
327    Parse(QpackEncoderInstructionsParseError),
328    /// Applying one validated instruction failed at this absolute byte offset.
329    Apply {
330        /// Absolute byte offset of the instruction that failed to apply.
331        offset: usize,
332        /// The instruction application failure.
333        error: QpackEncoderInstructionApplyError,
334    },
335}
336
337impl QpackEncoderInstructionsApplyError {
338    /// Returns whether this failure is an RFC QPACK encoder-stream error.
339    pub const fn is_encoder_stream_error(self) -> bool {
340        match self {
341            Self::Parse(_) => true,
342            Self::Apply { error, .. } => error.is_encoder_stream_error(),
343        }
344    }
345}
346
347impl fmt::Display for QpackEncoderInstructionsApplyError {
348    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349        match self {
350            Self::Parse(error) => {
351                write!(f, "QPACK encoder instruction sequence is invalid: {error}")
352            }
353            Self::Apply { offset, error } => {
354                write!(
355                    f,
356                    "QPACK encoder instruction application failed at byte {offset}: {error}"
357                )
358            }
359        }
360    }
361}
362
363impl core::error::Error for QpackEncoderInstructionsApplyError {}
364
365fn decoded_len(
366    literal: QpackStringLiteral<'_>,
367    name: bool,
368) -> Result<usize, QpackEncoderInstructionApplyError> {
369    if literal.is_huffman() {
370        QpackHuffmanDecoder::required_decoded_len(literal.encoded_payload()).map_err(|error| {
371            if name {
372                QpackEncoderInstructionApplyError::LiteralNameHuffman(error)
373            } else {
374                QpackEncoderInstructionApplyError::ValueHuffman(error)
375            }
376        })
377    } else {
378        Ok(literal.encoded_payload().len())
379    }
380}
381
382fn decode<'a>(
383    literal: QpackStringLiteral<'_>,
384    destination: &'a mut [u8],
385    name: bool,
386) -> Result<&'a [u8], QpackEncoderInstructionApplyError> {
387    QpackHuffmanDecoder::new(literal.encoded_payload(), destination)
388        .decode()
389        .map_err(|error| {
390            if name {
391                QpackEncoderInstructionApplyError::LiteralNameHuffman(error)
392            } else {
393                QpackEncoderInstructionApplyError::ValueHuffman(error)
394            }
395        })
396}
397
398fn checked_sum(left: usize, right: usize) -> Result<usize, QpackEncoderInstructionApplyError> {
399    left.checked_add(right)
400        .ok_or(QpackEncoderInstructionApplyError::ScratchLengthOverflow)
401}
402
403fn preflight_insert(
404    table: &QpackDynamicTable<'_, '_>,
405    name_len: usize,
406    value_len: usize,
407    required: usize,
408    available: usize,
409) -> Result<(), QpackEncoderInstructionApplyError> {
410    let entry_size = name_len
411        .checked_add(value_len)
412        .and_then(|length| length.checked_add(32))
413        .ok_or(QpackEncoderInstructionApplyError::Insert(
414            QpackDynamicTableError::EntrySizeOverflow,
415        ))?;
416    if entry_size > table.capacity() {
417        return Err(QpackEncoderInstructionApplyError::Insert(
418            QpackDynamicTableError::EntryLargerThanCapacity {
419                entry_size,
420                capacity: table.capacity(),
421            },
422        ));
423    }
424    if table.insert_count() == u64::MAX {
425        return Err(QpackEncoderInstructionApplyError::Insert(
426            QpackDynamicTableError::InsertCountOverflow,
427        ));
428    }
429    if available < required {
430        return Err(QpackEncoderInstructionApplyError::ScratchTooShort {
431            required,
432            available,
433        });
434    }
435    Ok(())
436}
437
438fn insert<F>(
439    table: &mut QpackDynamicTable<'_, '_>,
440    name: &[u8],
441    value: &[u8],
442    evictable: F,
443) -> Result<QpackEncoderInstructionApplyOutcome, QpackEncoderInstructionApplyError>
444where
445    F: FnMut(u64) -> bool,
446{
447    table
448        .insert(name, value, evictable)
449        .map(|absolute_index| QpackEncoderInstructionApplyOutcome::Inserted { absolute_index })
450        .map_err(QpackEncoderInstructionApplyError::Insert)
451}