1use std::{borrow::Cow, fmt};
6
7use serde::{Serialize, Serializer};
8
9use crate::{
10 http::proto::h3::{Code, FrameType as H3FrameType, SettingId, StreamType},
11 quic::proto::{
12 Version, frame::FrameType as QuicFrameType, transport_parameters::TransportParameterId,
13 },
14};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum CodepointName {
19 Known(Cow<'static, str>),
21 Grease,
23 Reserved,
25 Unknown,
27}
28
29impl CodepointName {
30 #[must_use]
32 pub fn as_str(&self) -> &str {
33 match self {
34 Self::Known(name) => name,
35 Self::Grease => "grease",
36 Self::Reserved => "reserved",
37 Self::Unknown => "unknown",
38 }
39 }
40
41 #[must_use]
43 pub fn label(&self, raw: u64) -> String {
44 match self {
45 Self::Known(name) => name.clone().into_owned(),
46 other => format!("{} {raw:#x}", other.as_str()),
47 }
48 }
49
50 fn from_parts(name: Option<&'static str>, grease: bool, reserved: bool) -> Self {
51 match name {
52 Some(name) => Self::Known(Cow::Borrowed(name)),
53 None if grease => Self::Grease,
54 None if reserved => Self::Reserved,
55 None => Self::Unknown,
56 }
57 }
58}
59
60impl fmt::Display for CodepointName {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(self.as_str())
63 }
64}
65
66impl Serialize for CodepointName {
67 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
68 serializer.serialize_str(self.as_str())
69 }
70}
71
72#[derive(Debug, Clone, Serialize)]
74pub struct Named<R> {
75 pub raw: R,
76 pub name: CodepointName,
77}
78
79#[must_use]
81pub fn quic_version_name(version: Version) -> CodepointName {
82 match version {
83 Version::V1 => CodepointName::Known(Cow::Borrowed("v1")),
84 Version::V2 => CodepointName::Known(Cow::Borrowed("v2")),
85 other if other.is_reserved() => CodepointName::Grease,
86 _ => CodepointName::Unknown,
87 }
88}
89
90#[must_use]
92pub fn named_quic_version(version: Version) -> Named<String> {
93 Named {
94 raw: format!("{:#010x}", version.as_u32()),
95 name: quic_version_name(version),
96 }
97}
98
99#[must_use]
101pub fn transport_parameter_name(id: TransportParameterId) -> CodepointName {
102 match id {
103 TransportParameterId::Unknown(_) if id.is_reserved() => CodepointName::Grease,
104 TransportParameterId::Unknown(_) => CodepointName::Unknown,
105 known => CodepointName::Known(Cow::Owned(snake_case(&known.variant_name()))),
106 }
107}
108
109#[must_use]
111pub fn named_transport_parameter(id: TransportParameterId) -> Named<u64> {
112 Named {
113 raw: u64::from(id),
114 name: transport_parameter_name(id),
115 }
116}
117
118#[must_use]
120pub fn quic_frame_name(frame_type: u64) -> CodepointName {
121 CodepointName::from_parts(QuicFrameType::from_u64(frame_type).name(), false, false)
122}
123
124#[must_use]
126pub fn h3_setting_name(id: u64) -> CodepointName {
127 let id = SettingId::new(id);
128 CodepointName::from_parts(id.name(), id.is_reserved(), id.is_h2_forbidden())
129}
130
131#[must_use]
133pub fn h3_stream_type_name(stream_type: u64) -> CodepointName {
134 let ty = StreamType::new(stream_type);
135 CodepointName::from_parts(ty.name(), ty.is_reserved(), false)
136}
137
138#[must_use]
140pub fn h3_frame_type_name(frame_type: u64) -> CodepointName {
141 let ty = H3FrameType::new(frame_type);
142 CodepointName::from_parts(ty.name(), ty.is_reserved(), ty.is_h2_reserved())
143}
144
145#[must_use]
147pub fn h3_error_name(code: u64) -> CodepointName {
148 let code = Code::new(code);
149 CodepointName::from_parts(code.name(), code.is_reserved(), false)
150}
151
152#[must_use]
154pub fn named(raw: u64, name_of: fn(u64) -> CodepointName) -> Named<u64> {
155 Named {
156 raw,
157 name: name_of(raw),
158 }
159}
160
161fn snake_case(name: &str) -> String {
162 let mut out = String::with_capacity(name.len() + 8);
163 for (index, c) in name.chars().enumerate() {
164 if c.is_ascii_uppercase() {
165 if index > 0 {
166 out.push('_');
167 }
168 out.push(c.to_ascii_lowercase());
169 } else {
170 out.push(c);
171 }
172 }
173 out
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use serde_json::{json, to_value};
180
181 #[test]
182 fn names_cover_known_grease_reserved_and_unknown_codepoints() {
183 assert_eq!(quic_version_name(Version::V1).as_str(), "v1");
184 assert_eq!(quic_version_name(Version::V2).as_str(), "v2");
185 assert_eq!(
186 quic_version_name(Version::from_u32(0x0a1a_2a3a)).as_str(),
187 "grease"
188 );
189 assert_eq!(
190 quic_version_name(Version::from_u32(0xff00_001d)).as_str(),
191 "unknown"
192 );
193 assert_eq!(named_quic_version(Version::V2).raw, "0x6b3343cf");
194
195 for (id, name) in [
196 (0x08, "initial_max_streams_bidi"),
197 (0x11, "version_information"),
198 (0x2ab2, "grease_quic_bit"),
199 (0xff04_de1b, "min_ack_delay_draft07"),
200 (31 * 5 + 27, "grease"),
201 (0x4243, "unknown"),
202 ] {
203 assert_eq!(
204 transport_parameter_name(TransportParameterId::from(id)).as_str(),
205 name
206 );
207 }
208
209 assert_eq!(quic_frame_name(0x06).as_str(), "CRYPTO");
210 assert_eq!(quic_frame_name(0x0a).as_str(), "STREAM");
211 assert_eq!(quic_frame_name(0x4243).as_str(), "unknown");
212
213 assert_eq!(h3_setting_name(0x06).as_str(), "MAX_FIELD_SECTION_SIZE");
214 assert_eq!(h3_setting_name(0x21).as_str(), "grease");
215 assert_eq!(h3_setting_name(0x02).as_str(), "reserved");
216 assert_eq!(h3_setting_name(0x4d45).as_str(), "unknown");
217 assert_eq!(h3_stream_type_name(0x02).as_str(), "QPACK_ENCODER");
218 assert_eq!(h3_frame_type_name(0x04).as_str(), "SETTINGS");
219 assert_eq!(h3_frame_type_name(0x06).as_str(), "reserved");
220 assert_eq!(h3_error_name(0x0101).as_str(), "H3_GENERAL_PROTOCOL_ERROR");
221
222 assert_eq!(h3_setting_name(0x21).label(0x21), "grease 0x21");
223 assert_eq!(
224 h3_setting_name(0x01).label(0x01),
225 "QPACK_MAX_TABLE_CAPACITY"
226 );
227 }
228
229 #[test]
230 fn named_serializes_raw_and_name() {
231 assert_eq!(
232 to_value(named(8, h3_setting_name)).unwrap(),
233 json!({"raw": 8, "name": "ENABLE_CONNECT_PROTOCOL"})
234 );
235 assert_eq!(
236 to_value(named_quic_version(Version::V1)).unwrap(),
237 json!({"raw": "0x00000001", "name": "v1"})
238 );
239 }
240}