Skip to main content

rama/cli/service/
wire_names.rs

1//! Display names for QUIC and HTTP/3 codepoints.
2//!
3//! Shared by the echo and fingerprint services, so both name a codepoint identically.
4
5use 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/// The display name of one protocol codepoint.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum CodepointName {
19    /// A registered name.
20    Known(Cow<'static, str>),
21    /// A value reserved for greasing.
22    Grease,
23    /// A value reserved by the registry without a meaning.
24    Reserved,
25    /// A value this implementation does not recognise.
26    Unknown,
27}
28
29impl CodepointName {
30    /// The name, or `grease`, `reserved` or `unknown`.
31    #[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    /// A label for human display: the known name, or the class with the hex value.
42    #[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/// A raw codepoint together with its display name.
73#[derive(Debug, Clone, Serialize)]
74pub struct Named<R> {
75    pub raw: R,
76    pub name: CodepointName,
77}
78
79/// A QUIC version: `v1`, `v2`, `grease` for `0x?a?a?a?a` (RFC 9000 §15) or `unknown`.
80#[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/// A QUIC version with its 32-bit wire value as hex text.
91#[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/// A transport parameter by its registry name, such as `initial_max_streams_bidi`.
100#[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/// A transport parameter identifier with its name.
110#[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/// A QUIC frame type (RFC 9000 §19).
119#[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/// An HTTP/3 setting identifier (RFC 9114 §7.2.4).
125#[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/// An HTTP/3 unidirectional stream type (RFC 9114 §6.2).
132#[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/// An HTTP/3 frame type (RFC 9114 §7.2).
139#[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/// An HTTP/3 error code (RFC 9114 §8.1).
146#[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/// Pair a raw value with the name `name_of` gives it.
153#[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}