Skip to main content

base_message/
static_table_entry.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::__private::{IncomingField, IncomingName};
5use crate::FieldSection;
6use crate::field_ref::IntoField;
7use std::fmt;
8use std::marker::PhantomData;
9
10/// How a name in a protocol's static table relates to a section.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum Classification {
13    /// The section declares the name, with this declaration id.
14    Declaration(u16),
15    /// The section forbids the name.
16    Forbidden,
17    /// The section neither declares nor forbids the name.
18    Undeclared,
19}
20
21/// One entry of a protocol's static table of field names, classified for section `S` by [`static_table_entries`].
22///
23/// An entry can only be made by [`static_table_entries`], and a section accepts only entries made for it, so a decoder cannot pass one section's classification to another. Its name is the table's own, so it cannot disagree with the classification either.
24pub struct StaticTableEntry<S> {
25    classification: Classification,
26    name: &'static str,
27    section: PhantomData<fn() -> S>,
28}
29
30impl<S> StaticTableEntry<S> {
31    /// How the section relates to this entry's name.
32    pub const fn classification(&self) -> Classification {
33        self.classification
34    }
35
36    /// The name in the table.
37    pub const fn name(&self) -> &'static str {
38        self.name
39    }
40
41    pub(crate) fn incoming_name(self) -> IncomingName<'static> {
42        match self.classification {
43            Classification::Declaration(declaration) => IncomingName::Declaration(declaration),
44            Classification::Forbidden => IncomingName::Forbidden(self.name),
45            Classification::Undeclared => IncomingName::Undeclared(self.name),
46        }
47    }
48}
49
50impl<S> Clone for StaticTableEntry<S> {
51    fn clone(&self) -> Self {
52        *self
53    }
54}
55
56impl<S> Copy for StaticTableEntry<S> {}
57
58impl<S> PartialEq for StaticTableEntry<S> {
59    fn eq(&self, other: &Self) -> bool {
60        (self.classification, self.name) == (other.classification, other.name)
61    }
62}
63
64impl<S> Eq for StaticTableEntry<S> {}
65
66impl<S> fmt::Debug for StaticTableEntry<S> {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        formatter
69            .debug_struct("StaticTableEntry")
70            .field("classification", &self.classification)
71            .field("name", &self.name)
72            .finish()
73    }
74}
75
76impl<'a, S: FieldSection<'a>> IntoField<'a, S> for (StaticTableEntry<S::Identity>, &'a [u8]) {
77    fn into_field(self, index: usize) -> IncomingField<'a> {
78        IncomingField {
79            position: index,
80            name: self.0.incoming_name(),
81            value: self.1,
82        }
83    }
84}
85
86/// Classify each name in a protocol's static table of lowercase field names, such as HPACK's or QPACK's, for section `S`, at compile time, so a decoder can interpret indexed fields without comparing names.
87///
88/// For a section with a lifetime, name it with `'static`, such as `Request<'static>`; its entries serve the section with any lifetime.
89///
90/// ```
91/// use base_message::{Classification, FieldSection, FieldType, FieldValueError, StaticTableEntry, ascii_text, combination::Singleton, static_table_entries};
92///
93/// struct Host(String);
94///
95/// impl FieldType<'_> for Host {
96///     type Combination = Singleton;
97///     fn parse(value: &[u8]) -> Result<Self, FieldValueError> {
98///         Ok(Self(ascii_text(value)?.to_owned()))
99///     }
100/// }
101///
102/// #[derive(FieldSection)]
103/// #[fields(forbid("transfer-encoding"))]
104/// struct Request {
105///     #[field("host")]
106///     host: Option<Host>,
107/// }
108///
109/// const NAMES: [&str; 3] = ["host", "accept", "transfer-encoding"];
110/// const TABLE: [StaticTableEntry<Request>; 3] = static_table_entries::<Request, 3>(&NAMES);
111/// let classifications = TABLE.map(|entry| entry.classification());
112/// assert_eq!(classifications, [Classification::Declaration(0), Classification::Undeclared, Classification::Forbidden]);
113///
114/// let request = Request::interpret([(TABLE[0], &b"example.com"[..])]).unwrap();
115/// assert_eq!(request.host.map(|host| host.0).as_deref(), Some("example.com"));
116/// ```
117///
118/// # Panics
119///
120/// When a name in the table is not lowercase visible ASCII, such as `"Transfer-Encoding"`, since a section's names are lowercase and such a name would never match the declaration or forbidden name it spells; or when a name has a declaration id above 65,535. A constant reports either as a compile-time error.
121pub const fn static_table_entries<S: FieldSection<'static>, const N: usize>(
122    table: &[&'static str; N],
123) -> [StaticTableEntry<S::Identity>; N] {
124    let mut entries = [StaticTableEntry {
125        classification: Classification::Undeclared,
126        name: "",
127        section: PhantomData,
128    }; N];
129    let mut index = 0;
130    while index < N {
131        assert!(
132            lowercase(table[index]),
133            "a static table's names must be lowercase visible ASCII, as HPACK's and QPACK's are"
134        );
135        let classification = if let Some(declaration) = S::NAMES.declaration(table[index]) {
136            assert!(
137                declaration <= u16::MAX as usize,
138                "a static table classifies declaration ids up to 65,535"
139            );
140            #[allow(
141                clippy::cast_possible_truncation,
142                reason = "the assertion above bounds the id"
143            )]
144            Classification::Declaration(declaration as u16)
145        } else if S::NAMES.forbids(table[index]) {
146            Classification::Forbidden
147        } else {
148            Classification::Undeclared
149        };
150        entries[index] = StaticTableEntry {
151            classification,
152            name: table[index],
153            section: PhantomData,
154        };
155        index += 1;
156    }
157    entries
158}
159
160/// Whether a name is non-empty visible ASCII without uppercase letters. This admits field names and pseudo-header names such as `:authority`.
161const fn lowercase(name: &str) -> bool {
162    let bytes = name.as_bytes();
163    let mut index = 0;
164    while index < bytes.len() {
165        if !bytes[index].is_ascii_graphic() || bytes[index].is_ascii_uppercase() {
166            return false;
167        }
168        index += 1;
169    }
170    !bytes.is_empty()
171}