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}