Skip to main content

net_wire/qpack/field/section/
encode.rs

1//! Transactional allocation-free QPACK field-section encoding.
2
3use core::fmt;
4
5use super::context::{QpackFieldSectionContext, QpackFieldSectionContextError};
6use super::prefix::{QpackFieldSectionPrefixBuildError, plan_field_section_prefix};
7use crate::qpack::field::line::{
8    QpackFieldLineBuildError, QpackFieldLinePlan, QpackFieldLinePlanInput, plan_field_line,
9};
10use crate::qpack::state::encoder::{QpackEncoderState, QpackEncoderStateError};
11use crate::qpack::table::{QPACK_STATIC_TABLE_LEN, QpackDynamicTable, QpackDynamicTableError};
12
13/// An allocation-free QPACK field-section encoder configuration.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct QpackFieldSectionEncoder {
16    maximum_dynamic_table_capacity: u64,
17}
18
19impl QpackFieldSectionEncoder {
20    /// Creates an encoder using the peer's maximum dynamic-table capacity.
21    pub const fn new(maximum_dynamic_table_capacity: u64) -> Self {
22        Self {
23            maximum_dynamic_table_capacity,
24        }
25    }
26
27    /// Returns the peer's maximum dynamic-table capacity.
28    pub const fn maximum_dynamic_table_capacity(self) -> u64 {
29        self.maximum_dynamic_table_capacity
30    }
31
32    /// Encodes one field section, retaining dynamic references only after its bytes are written.
33    ///
34    /// Capacity failures leave both scratch slices unchanged. Later failures can overwrite scratch,
35    /// but never modify `destination` or encoder accounting. Each semantic input needs one plan
36    /// slot; plan slots are opaque overwritten scratch and hold no semantic value on return.
37    pub fn encode<'plan, 'output>(
38        &self,
39        stream_id: u64,
40        plans: &[QpackFieldPlan<'plan>],
41        base: QpackFieldSectionBase,
42        table: &QpackDynamicTable<'_, '_>,
43        state: &mut QpackEncoderState<'_, '_>,
44        buffers: QpackFieldSectionEncodeBuffers<'_, 'plan, 'output>,
45    ) -> Result<QpackEncodedFieldSection<'output>, QpackFieldSectionEncodeError> {
46        let QpackFieldSectionEncodeBuffers {
47            reference_scratch,
48            field_plan_scratch,
49            destination,
50        } = buffers;
51        let reference_count = dynamic_occurrence_count(plans)?;
52        if reference_scratch.len() < reference_count {
53            return Err(QpackFieldSectionEncodeError::ReferenceScratchTooShort {
54                required: reference_count,
55                available: reference_scratch.len(),
56            });
57        }
58        if field_plan_scratch.len() < plans.len() {
59            return Err(QpackFieldSectionEncodeError::FieldPlanScratchTooShort {
60                required: plans.len(),
61                available: field_plan_scratch.len(),
62            });
63        }
64
65        let references = &mut reference_scratch[..reference_count];
66        let mut reference_index = 0usize;
67        for (field_index, plan) in plans.iter().enumerate() {
68            if let Some(absolute_index) = dynamic_absolute(plan) {
69                table.get_absolute(absolute_index).map_err(|error| {
70                    QpackFieldSectionEncodeError::Plan(
71                        QpackFieldSectionPlanError::DynamicReference { field_index, error },
72                    )
73                })?;
74                references[reference_index] = absolute_index;
75                reference_index += 1;
76            } else if let Some(index) = static_index(plan)
77                && index >= QPACK_STATIC_TABLE_LEN as u64
78            {
79                return Err(QpackFieldSectionEncodeError::Plan(
80                    QpackFieldSectionPlanError::StaticIndexOutOfRange { field_index, index },
81                ));
82            }
83        }
84
85        let required_insert_count = required_insert_count(references)?;
86        let insert_count = table.insert_count();
87        let base = match base {
88            QpackFieldSectionBase::RequiredInsertCount => required_insert_count,
89            QpackFieldSectionBase::Explicit(base) => {
90                if base > insert_count {
91                    return Err(QpackFieldSectionEncodeError::BaseExceedsInsertCount {
92                        base,
93                        insert_count,
94                    });
95                }
96                base
97            }
98        };
99        let (negative, delta_base) = prefix_delta(required_insert_count, base);
100        let encoded_required_insert_count = QpackFieldSectionContext::encode_required_insert_count(
101            required_insert_count,
102            self.maximum_dynamic_table_capacity,
103        )
104        .map_err(QpackFieldSectionEncodeError::RequiredInsertCountEncoding)?;
105        let prefix = plan_field_section_prefix(encoded_required_insert_count, negative, delta_base)
106            .map_err(QpackFieldSectionEncodeError::PrefixPlanning)?;
107
108        let mut total = prefix.encoded_len();
109        for (field_index, input) in plans.iter().enumerate() {
110            let input = field_line_input(*input, base);
111            let wire_plan = plan_field_line(input).map_err(|error| {
112                QpackFieldSectionEncodeError::Plan(QpackFieldSectionPlanError::FieldLine {
113                    field_index,
114                    error,
115                })
116            })?;
117            total = total
118                .checked_add(wire_plan.encoded_len())
119                .ok_or(QpackFieldSectionEncodeError::OutputLengthOverflow)?;
120            field_plan_scratch[field_index].wire_plan = wire_plan;
121        }
122        if destination.len() < total {
123            return Err(QpackFieldSectionEncodeError::DestinationTooShort {
124                required: total,
125                available: destination.len(),
126            });
127        }
128
129        let reservation = state
130            .reserve_registration(stream_id, required_insert_count, references, insert_count)
131            .map_err(QpackFieldSectionEncodeError::EncoderStateReservation)?;
132        let (prefix_destination, mut fields_destination) =
133            destination[..total].split_at_mut(prefix.encoded_len());
134        prefix.write(prefix_destination);
135        for slot in &field_plan_scratch[..plans.len()] {
136            let length = slot.wire_plan.encoded_len();
137            let (field_destination, remaining) = fields_destination.split_at_mut(length);
138            slot.wire_plan.write(field_destination);
139            fields_destination = remaining;
140        }
141        reservation.commit();
142        Ok(QpackEncodedFieldSection {
143            bytes: &destination[..total],
144            required_insert_count,
145            base,
146        })
147    }
148}
149
150/// Caller-owned workspace used by one field-section encoding operation.
151pub struct QpackFieldSectionEncodeBuffers<'scratch, 'plan, 'output> {
152    reference_scratch: &'scratch mut [u64],
153    field_plan_scratch: &'scratch mut [QpackFieldSectionPlanSlot<'plan>],
154    destination: &'output mut [u8],
155}
156
157impl<'scratch, 'plan, 'output> QpackFieldSectionEncodeBuffers<'scratch, 'plan, 'output> {
158    /// Creates an encoding workspace from reference, wire-plan, and output storage.
159    pub fn new(
160        reference_scratch: &'scratch mut [u64],
161        field_plan_scratch: &'scratch mut [QpackFieldSectionPlanSlot<'plan>],
162        destination: &'output mut [u8],
163    ) -> Self {
164        Self {
165            reference_scratch,
166            field_plan_scratch,
167            destination,
168        }
169    }
170}
171
172/// One semantic field representation supplied to [`QpackFieldSectionEncoder`].
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum QpackFieldPlan<'a> {
175    /// An indexed reference to the static table.
176    IndexedStatic {
177        /// Zero-based static-table index.
178        index: u64,
179    },
180    /// An indexed reference to the dynamic table by zero-based absolute index.
181    IndexedDynamic {
182        /// Zero-based dynamic absolute index.
183        absolute_index: u64,
184    },
185    /// A literal with a static-table name reference.
186    LiteralStaticNameReference {
187        /// Whether the field must not be inserted by a recipient.
188        never_indexed: bool,
189        /// Zero-based static-table name index.
190        name_index: u64,
191        /// Whether `encoded_value` has Huffman coding.
192        value_huffman: bool,
193        /// Opaque already-encoded value bytes.
194        encoded_value: &'a [u8],
195    },
196    /// A literal with a dynamic-table name reference by zero-based absolute index.
197    LiteralDynamicNameReference {
198        /// Whether the field must not be inserted by a recipient.
199        never_indexed: bool,
200        /// Zero-based dynamic absolute name index.
201        name_absolute_index: u64,
202        /// Whether `encoded_value` has Huffman coding.
203        value_huffman: bool,
204        /// Opaque already-encoded value bytes.
205        encoded_value: &'a [u8],
206    },
207    /// A literal name and value with already-encoded opaque payloads.
208    LiteralName {
209        /// Whether the field must not be inserted by a recipient.
210        never_indexed: bool,
211        /// Whether `encoded_name` has Huffman coding.
212        name_huffman: bool,
213        /// Opaque already-encoded name bytes.
214        encoded_name: &'a [u8],
215        /// Whether `encoded_value` has Huffman coding.
216        value_huffman: bool,
217        /// Opaque already-encoded value bytes.
218        encoded_value: &'a [u8],
219    },
220}
221
222/// Selects the field section Base used to translate dynamic absolute indexes.
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub enum QpackFieldSectionBase {
225    /// Use Required Insert Count as the Base.
226    RequiredInsertCount,
227    /// Use this explicit Base.
228    Explicit(#[doc = "The explicit Base."] u64),
229}
230
231/// Opaque caller-owned storage for one retained field-line wire plan.
232///
233/// Initialize arrays with [`Self::EMPTY`]. Supply at least one slot for every semantic plan.
234/// Slots are overwritten scratch and have no semantic value after encoding returns.
235#[derive(Clone, Copy)]
236pub struct QpackFieldSectionPlanSlot<'a> {
237    wire_plan: QpackFieldLinePlan<'a>,
238}
239impl<'a> QpackFieldSectionPlanSlot<'a> {
240    /// Empty opaque storage suitable for caller array initialization.
241    pub const EMPTY: Self = Self {
242        wire_plan: QpackFieldLinePlan::empty(),
243    };
244}
245
246/// The exact encoded field-section bytes and resolved context.
247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248pub struct QpackEncodedFieldSection<'a> {
249    bytes: &'a [u8],
250    required_insert_count: u64,
251    base: u64,
252}
253impl<'a> QpackEncodedFieldSection<'a> {
254    /// Returns the complete encoded prefix and field lines.
255    pub const fn as_bytes(self) -> &'a [u8] {
256        self.bytes
257    }
258    /// Returns the semantic Required Insert Count.
259    pub const fn required_insert_count(self) -> u64 {
260        self.required_insert_count
261    }
262    /// Returns the resolved Base.
263    pub const fn base(self) -> u64 {
264        self.base
265    }
266    /// Returns the encoded byte length.
267    pub const fn len(self) -> usize {
268        self.bytes.len()
269    }
270    /// Returns whether no bytes were encoded.
271    pub const fn is_empty(self) -> bool {
272        self.bytes.is_empty()
273    }
274}
275
276/// A field-specific planning failure.
277#[derive(Clone, Copy, Debug, Eq, PartialEq)]
278pub enum QpackFieldSectionPlanError {
279    /// A static-table index is outside RFC 9204 Appendix A.
280    StaticIndexOutOfRange {
281        /// Index of the semantic field plan.
282        field_index: usize,
283        /// Invalid static-table index.
284        index: u64,
285    },
286    /// A dynamic absolute index cannot be resolved from the active dynamic table.
287    DynamicReference {
288        /// Index of the semantic field plan.
289        field_index: usize,
290        /// Nested dynamic-table lookup failure.
291        error: QpackDynamicTableError,
292    },
293
294    /// A field-line wire representation cannot be planned.
295    FieldLine {
296        /// Index of the semantic field plan.
297        field_index: usize,
298        /// Nested field-line planning failure.
299        error: QpackFieldLineBuildError,
300    },
301}
302impl fmt::Display for QpackFieldSectionPlanError {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        match self {
305            Self::StaticIndexOutOfRange { field_index, index } => write!(
306                f,
307                "QPACK field {field_index} has static-table index {index} outside the table"
308            ),
309            Self::DynamicReference { field_index, error } => write!(
310                f,
311                "QPACK field {field_index} has an invalid dynamic reference: {error}"
312            ),
313
314            Self::FieldLine { field_index, error } => {
315                write!(f, "QPACK field {field_index} cannot be planned: {error}")
316            }
317        }
318    }
319}
320impl core::error::Error for QpackFieldSectionPlanError {}
321
322/// Failure to encode one QPACK field section.
323#[derive(Clone, Copy, Debug, Eq, PartialEq)]
324pub enum QpackFieldSectionEncodeError {
325    /// Counting dynamic-reference occurrences overflowed `usize`.
326    ReferenceCountOverflow,
327    /// The caller supplied too little reference scratch.
328    ReferenceScratchTooShort {
329        /// Required number of reference slots.
330        required: usize,
331        /// Available reference slots.
332        available: usize,
333    },
334    /// The caller supplied too little retained field-plan scratch.
335    FieldPlanScratchTooShort {
336        /// Required number of plan slots.
337        required: usize,
338        /// Available plan slots.
339        available: usize,
340    },
341    /// One semantic field plan is invalid.
342    Plan(QpackFieldSectionPlanError),
343    /// Adding one to the largest dynamic absolute index overflowed `u64`.
344    RequiredInsertCountOverflow {
345        /// Largest zero-based absolute index.
346        absolute_index: u64,
347    },
348    /// The explicit Base is greater than the dynamic table insert count.
349    BaseExceedsInsertCount {
350        /// Requested Base.
351        base: u64,
352        /// Current dynamic-table insert count.
353        insert_count: u64,
354    },
355
356    /// Required Insert Count context encoding failed.
357    RequiredInsertCountEncoding(QpackFieldSectionContextError),
358    /// Prefix wire planning failed.
359    PrefixPlanning(QpackFieldSectionPrefixBuildError),
360    /// Summing the complete output length overflowed `usize`.
361    OutputLengthOverflow,
362    /// The destination is too short for the complete field section.
363    DestinationTooShort {
364        /// Required output bytes.
365        required: usize,
366        /// Available output bytes.
367        available: usize,
368    },
369    /// Encoder accounting could not reserve the dynamic references.
370    EncoderStateReservation(QpackEncoderStateError),
371}
372impl QpackFieldSectionEncodeError {
373    /// Returns whether this failure is caused by insufficient caller-owned scratch or output storage.
374    pub const fn is_provisioning_error(self) -> bool {
375        matches!(
376            self,
377            Self::ReferenceScratchTooShort { .. }
378                | Self::FieldPlanScratchTooShort { .. }
379                | Self::DestinationTooShort { .. }
380        )
381    }
382}
383impl fmt::Display for QpackFieldSectionEncodeError {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        match self {
386            Self::ReferenceCountOverflow => {
387                f.write_str("QPACK dynamic reference count overflows usize")
388            }
389            Self::ReferenceScratchTooShort {
390                required,
391                available,
392            } => write!(
393                f,
394                "QPACK reference scratch needs {required} slots, has {available}"
395            ),
396            Self::FieldPlanScratchTooShort {
397                required,
398                available,
399            } => write!(
400                f,
401                "QPACK field-plan scratch needs {required} slots, has {available}"
402            ),
403            Self::Plan(error) => write!(f, "QPACK field-section plan is invalid: {error}"),
404            Self::RequiredInsertCountOverflow { absolute_index } => write!(
405                f,
406                "QPACK Required Insert Count overflows for absolute index {absolute_index}"
407            ),
408            Self::BaseExceedsInsertCount { base, insert_count } => {
409                write!(f, "QPACK Base {base} exceeds insert count {insert_count}")
410            }
411
412            Self::RequiredInsertCountEncoding(error) => {
413                write!(f, "QPACK Required Insert Count cannot be encoded: {error}")
414            }
415            Self::PrefixPlanning(error) => {
416                write!(f, "QPACK field-section prefix cannot be planned: {error}")
417            }
418            Self::OutputLengthOverflow => {
419                f.write_str("QPACK field-section output length overflows usize")
420            }
421            Self::DestinationTooShort {
422                required,
423                available,
424            } => write!(
425                f,
426                "QPACK field-section destination needs {required} bytes, has {available}"
427            ),
428            Self::EncoderStateReservation(error) => write!(
429                f,
430                "QPACK encoder state cannot reserve field section: {error}"
431            ),
432        }
433    }
434}
435impl core::error::Error for QpackFieldSectionEncodeError {}
436
437fn dynamic_occurrence_count(
438    plans: &[QpackFieldPlan<'_>],
439) -> Result<usize, QpackFieldSectionEncodeError> {
440    let mut count = 0usize;
441    for plan in plans {
442        if dynamic_absolute(plan).is_some() {
443            count = count
444                .checked_add(1)
445                .ok_or(QpackFieldSectionEncodeError::ReferenceCountOverflow)?;
446        }
447    }
448    Ok(count)
449}
450fn dynamic_absolute(plan: &QpackFieldPlan<'_>) -> Option<u64> {
451    match *plan {
452        QpackFieldPlan::IndexedDynamic { absolute_index } => Some(absolute_index),
453        QpackFieldPlan::LiteralDynamicNameReference {
454            name_absolute_index,
455            ..
456        } => Some(name_absolute_index),
457        _ => None,
458    }
459}
460fn static_index(plan: &QpackFieldPlan<'_>) -> Option<u64> {
461    match *plan {
462        QpackFieldPlan::IndexedStatic { index } => Some(index),
463        QpackFieldPlan::LiteralStaticNameReference { name_index, .. } => Some(name_index),
464        _ => None,
465    }
466}
467fn required_insert_count(references: &[u64]) -> Result<u64, QpackFieldSectionEncodeError> {
468    let mut largest = match references.first() {
469        Some(value) => *value,
470        None => return Ok(0),
471    };
472    for &reference in &references[1..] {
473        if reference > largest {
474            largest = reference;
475        }
476    }
477    largest
478        .checked_add(1)
479        .ok_or(QpackFieldSectionEncodeError::RequiredInsertCountOverflow {
480            absolute_index: largest,
481        })
482}
483fn prefix_delta(required_insert_count: u64, base: u64) -> (bool, u64) {
484    if base >= required_insert_count {
485        return (false, base - required_insert_count);
486    }
487    (true, required_insert_count - base - 1)
488}
489fn field_line_input<'a>(plan: QpackFieldPlan<'a>, base: u64) -> QpackFieldLinePlanInput<'a> {
490    match plan {
491        QpackFieldPlan::IndexedStatic { index } => QpackFieldLinePlanInput::IndexedStatic { index },
492        QpackFieldPlan::IndexedDynamic { absolute_index } => {
493            dynamic_indexed_input(absolute_index, base)
494        }
495        QpackFieldPlan::LiteralStaticNameReference {
496            never_indexed,
497            name_index,
498            value_huffman,
499            encoded_value,
500        } => QpackFieldLinePlanInput::LiteralStaticNameReference {
501            never_indexed,
502            index: name_index,
503            value_huffman,
504            encoded_value,
505        },
506        QpackFieldPlan::LiteralDynamicNameReference {
507            never_indexed,
508            name_absolute_index,
509            value_huffman,
510            encoded_value,
511        } => dynamic_name_input(
512            never_indexed,
513            name_absolute_index,
514            value_huffman,
515            encoded_value,
516            base,
517        ),
518        QpackFieldPlan::LiteralName {
519            never_indexed,
520            name_huffman,
521            encoded_name,
522            value_huffman,
523            encoded_value,
524        } => QpackFieldLinePlanInput::LiteralName {
525            never_indexed,
526            name_huffman,
527            encoded_name,
528            value_huffman,
529            encoded_value,
530        },
531    }
532}
533fn dynamic_indexed_input(absolute_index: u64, base: u64) -> QpackFieldLinePlanInput<'static> {
534    if absolute_index < base {
535        QpackFieldLinePlanInput::IndexedDynamicPreBase {
536            relative_index: base - absolute_index - 1,
537        }
538    } else {
539        QpackFieldLinePlanInput::IndexedDynamicPostBase {
540            post_base_index: absolute_index - base,
541        }
542    }
543}
544fn dynamic_name_input<'a>(
545    never_indexed: bool,
546    absolute_index: u64,
547    value_huffman: bool,
548    encoded_value: &'a [u8],
549    base: u64,
550) -> QpackFieldLinePlanInput<'a> {
551    if absolute_index < base {
552        QpackFieldLinePlanInput::LiteralDynamicPreBaseNameReference {
553            never_indexed,
554            relative_index: base - absolute_index - 1,
555            value_huffman,
556            encoded_value,
557        }
558    } else {
559        QpackFieldLinePlanInput::LiteralDynamicPostBaseNameReference {
560            never_indexed,
561            post_base_index: absolute_index - base,
562            value_huffman,
563            encoded_value,
564        }
565    }
566}