turbo_debug_console/
proto.rs1pub const NAME_MAX: usize = 64;
19
20pub const PROTOCOL_VERSION: u32 = 1;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum StreamKind {
27 Tokens,
29 Trace,
31}
32
33impl StreamKind {
34 pub fn parse(s: &str) -> Result<Self, HelloError> {
39 match s {
40 "tokens" => Ok(Self::Tokens),
41 "trace" => Ok(Self::Trace),
42 other => Err(HelloError::UnknownStreamKind(other.to_string())),
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum HelloError {
50 NotHello,
52 BadName,
54 MissingVersion,
58 BadVersion,
60 UnsupportedVersion(u32),
62 MissingStreamKind,
66 UnknownStreamKind(String),
68}
69
70impl HelloError {
71 #[must_use]
73 pub fn wire(&self) -> String {
74 match self {
75 Self::NotHello => "ERR not a handshake".to_string(),
76 Self::BadName => "ERR bad name".to_string(),
77 Self::MissingVersion => "ERR missing protocol version".to_string(),
78 Self::BadVersion => "ERR bad protocol version".to_string(),
79 Self::UnsupportedVersion(v) => format!("ERR unsupported protocol version {v}"),
80 Self::MissingStreamKind => "ERR missing stream kind".to_string(),
81 Self::UnknownStreamKind(k) => format!("ERR unknown stream kind {k}"),
82 }
83 }
84}
85
86pub fn parse_hello(line: &str) -> Result<(StreamKind, String), HelloError> {
99 let line = line.trim_end_matches(['\r', '\n']);
100 let rest = line.strip_prefix("HELLO ").ok_or(HelloError::NotHello)?;
101
102 let (version, rest) = rest.split_once(' ').ok_or(HelloError::MissingVersion)?;
103 if version.is_empty() {
104 return Err(HelloError::MissingVersion);
105 }
106 let version: u32 = version.parse().map_err(|_| HelloError::BadVersion)?;
107 if version != PROTOCOL_VERSION {
108 return Err(HelloError::UnsupportedVersion(version));
109 }
110
111 let (kind, name) = rest.split_once(' ').ok_or(HelloError::MissingStreamKind)?;
112 let kind = StreamKind::parse(kind)?;
113
114 if name.is_empty() || name.len() > NAME_MAX {
115 return Err(HelloError::BadName);
116 }
117 if !name.bytes().all(|b| (0x21..=0x7e).contains(&b)) {
118 return Err(HelloError::BadName);
119 }
120 Ok((kind, name.to_string()))
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn accepts_a_well_formed_tokens_hello() {
129 assert_eq!(
130 parse_hello("HELLO 1 tokens build-agent").unwrap(),
131 (StreamKind::Tokens, "build-agent".to_string())
132 );
133 }
134
135 #[test]
136 fn accepts_a_well_formed_trace_hello() {
137 assert_eq!(
138 parse_hello("HELLO 1 trace myapp").unwrap(),
139 (StreamKind::Trace, "myapp".to_string())
140 );
141 }
142
143 #[test]
144 fn trailing_cr_is_tolerated() {
145 assert_eq!(
146 parse_hello("HELLO 1 tokens x\r").unwrap(),
147 (StreamKind::Tokens, "x".to_string())
148 );
149 }
150
151 #[test]
152 fn a_non_hello_line_is_not_an_error_but_a_raw_stream() {
153 assert!(matches!(
154 parse_hello("hello there"),
155 Err(HelloError::NotHello)
156 ));
157 assert!(matches!(
158 parse_hello("{\"tok\":1}"),
159 Err(HelloError::NotHello)
160 ));
161 }
162
163 #[test]
164 fn hello_with_no_version_is_missing_version_not_assumed_v1() {
165 assert!(matches!(
166 parse_hello("HELLO build-agent"),
167 Err(HelloError::MissingVersion)
168 ));
169 }
170
171 #[test]
172 fn non_numeric_version_is_bad_version_not_a_fallback() {
173 assert!(matches!(
174 parse_hello("HELLO v1 tokens build-agent"),
175 Err(HelloError::BadVersion)
176 ));
177 assert!(matches!(
178 parse_hello("HELLO -1 tokens build-agent"),
179 Err(HelloError::BadVersion)
180 ));
181 }
182
183 #[test]
184 fn unsupported_version_is_rejected_by_number() {
185 assert_eq!(
186 parse_hello("HELLO 2 tokens build-agent"),
187 Err(HelloError::UnsupportedVersion(2))
188 );
189 assert_eq!(
190 HelloError::UnsupportedVersion(2).wire(),
191 "ERR unsupported protocol version 2"
192 );
193 }
194
195 #[test]
199 fn the_old_two_field_form_is_missing_stream_kind() {
200 assert_eq!(
201 parse_hello("HELLO 1 build-agent"),
202 Err(HelloError::MissingStreamKind)
203 );
204 assert_eq!(
205 HelloError::MissingStreamKind.wire(),
206 "ERR missing stream kind"
207 );
208 }
209
210 #[test]
211 fn an_unknown_stream_kind_is_rejected_by_name() {
212 assert_eq!(
213 parse_hello("HELLO 1 bogus build-agent"),
214 Err(HelloError::UnknownStreamKind("bogus".to_string()))
215 );
216 assert_eq!(
217 HelloError::UnknownStreamKind("bogus".to_string()).wire(),
218 "ERR unknown stream kind bogus"
219 );
220 }
221
222 #[test]
223 fn empty_oversized_and_whitespace_names_are_rejected() {
224 assert!(matches!(
225 parse_hello("HELLO 1 tokens "),
226 Err(HelloError::BadName)
227 ));
228 assert!(matches!(
229 parse_hello("HELLO 1 tokens a b"),
230 Err(HelloError::BadName)
231 ));
232 let long = "x".repeat(65);
233 assert!(matches!(
234 parse_hello(&format!("HELLO 1 tokens {long}")),
235 Err(HelloError::BadName)
236 ));
237 assert!(parse_hello(&format!("HELLO 1 tokens {}", "x".repeat(64))).is_ok());
238 }
239
240 #[test]
241 fn non_printable_names_are_rejected() {
242 assert!(matches!(
243 parse_hello("HELLO 1 tokens na\u{7}me"),
244 Err(HelloError::BadName)
245 ));
246 assert!(matches!(
247 parse_hello("HELLO 1 tokens café"),
248 Err(HelloError::BadName)
249 ));
250 }
251}