turbo_debug_console/
proto.rs1pub const NAME_MAX: usize = 64;
17
18pub const PROTOCOL_VERSION: u32 = 1;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum HelloError {
25 NotHello,
27 BadName,
29 MissingVersion,
33 BadVersion,
35 UnsupportedVersion(u32),
37}
38
39impl HelloError {
40 #[must_use]
42 pub fn wire(&self) -> String {
43 match self {
44 Self::NotHello => "ERR not a handshake".to_string(),
45 Self::BadName => "ERR bad name".to_string(),
46 Self::MissingVersion => "ERR missing protocol version".to_string(),
47 Self::BadVersion => "ERR bad protocol version".to_string(),
48 Self::UnsupportedVersion(v) => format!("ERR unsupported protocol version {v}"),
49 }
50 }
51}
52
53pub fn parse_hello(line: &str) -> Result<String, HelloError> {
64 let line = line.trim_end_matches(['\r', '\n']);
65 let rest = line.strip_prefix("HELLO ").ok_or(HelloError::NotHello)?;
66
67 let (version, name) = rest.split_once(' ').ok_or(HelloError::MissingVersion)?;
68 if version.is_empty() {
69 return Err(HelloError::MissingVersion);
70 }
71 let version: u32 = version.parse().map_err(|_| HelloError::BadVersion)?;
72 if version != PROTOCOL_VERSION {
73 return Err(HelloError::UnsupportedVersion(version));
74 }
75
76 if name.is_empty() || name.len() > NAME_MAX {
77 return Err(HelloError::BadName);
78 }
79 if !name.bytes().all(|b| (0x21..=0x7e).contains(&b)) {
80 return Err(HelloError::BadName);
81 }
82 Ok(name.to_string())
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn accepts_a_well_formed_hello() {
91 assert_eq!(parse_hello("HELLO 1 build-agent").unwrap(), "build-agent");
92 }
93
94 #[test]
95 fn trailing_cr_is_tolerated() {
96 assert_eq!(parse_hello("HELLO 1 x\r").unwrap(), "x");
97 }
98
99 #[test]
100 fn a_non_hello_line_is_not_an_error_but_a_raw_stream() {
101 assert!(matches!(
102 parse_hello("hello there"),
103 Err(HelloError::NotHello)
104 ));
105 assert!(matches!(
106 parse_hello("{\"tok\":1}"),
107 Err(HelloError::NotHello)
108 ));
109 }
110
111 #[test]
112 fn hello_with_no_version_is_missing_version_not_assumed_v1() {
113 assert!(matches!(
114 parse_hello("HELLO build-agent"),
115 Err(HelloError::MissingVersion)
116 ));
117 }
118
119 #[test]
120 fn non_numeric_version_is_bad_version_not_a_fallback() {
121 assert!(matches!(
122 parse_hello("HELLO v1 build-agent"),
123 Err(HelloError::BadVersion)
124 ));
125 assert!(matches!(
126 parse_hello("HELLO -1 build-agent"),
127 Err(HelloError::BadVersion)
128 ));
129 }
130
131 #[test]
132 fn unsupported_version_is_rejected_by_number() {
133 assert_eq!(
134 parse_hello("HELLO 2 build-agent"),
135 Err(HelloError::UnsupportedVersion(2))
136 );
137 assert_eq!(
138 HelloError::UnsupportedVersion(2).wire(),
139 "ERR unsupported protocol version 2"
140 );
141 }
142
143 #[test]
144 fn empty_oversized_and_whitespace_names_are_rejected() {
145 assert!(matches!(parse_hello("HELLO 1 "), Err(HelloError::BadName)));
146 assert!(matches!(
147 parse_hello("HELLO 1 a b"),
148 Err(HelloError::BadName)
149 ));
150 let long = "x".repeat(65);
151 assert!(matches!(
152 parse_hello(&format!("HELLO 1 {long}")),
153 Err(HelloError::BadName)
154 ));
155 assert!(parse_hello(&format!("HELLO 1 {}", "x".repeat(64))).is_ok());
156 }
157
158 #[test]
159 fn non_printable_names_are_rejected() {
160 assert!(matches!(
161 parse_hello("HELLO 1 na\u{7}me"),
162 Err(HelloError::BadName)
163 ));
164 assert!(matches!(
165 parse_hello("HELLO 1 café"),
166 Err(HelloError::BadName)
167 ));
168 }
169}