1use std::fmt;
14use std::str::FromStr;
15
16use crate::codes::{ErrorCode, WarningCode};
17use crate::error::{ParseError, Result};
18use crate::tokenize::{quote_value, strip_quotes, Tokenizer};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct StateToken(pub [u8; 8]);
23
24impl StateToken {
25 pub const ZERO: Self = Self([0; 8]);
28
29 #[must_use]
31 pub const fn from_bytes(bytes: [u8; 8]) -> Self {
32 Self(bytes)
33 }
34}
35
36impl fmt::Display for StateToken {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 for b in self.0 {
39 write!(f, "{b:02x}")?;
40 }
41 Ok(())
42 }
43}
44
45impl FromStr for StateToken {
46 type Err = ParseError;
47 fn from_str(s: &str) -> Result<Self> {
48 if s.len() != 16
49 || !s
50 .bytes()
51 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
52 {
53 return Err(ParseError::InvalidToken { raw: s.to_string() });
54 }
55 let mut bytes = [0u8; 8];
56 for (i, chunk) in s.as_bytes().chunks_exact(2).enumerate() {
57 bytes[i] = u8::from_str_radix(std::str::from_utf8(chunk).expect("ascii"), 16)
59 .map_err(|_| ParseError::InvalidToken { raw: s.to_string() })?;
60 }
61 Ok(Self(bytes))
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Warning {
68 pub code: WarningCode,
69 pub args: Vec<String>,
70}
71
72impl Warning {
73 #[must_use]
74 pub fn new(code: WarningCode) -> Self {
75 Self {
76 code,
77 args: Vec::new(),
78 }
79 }
80
81 #[must_use]
82 pub fn with_args(code: WarningCode, args: Vec<String>) -> Self {
83 Self { code, args }
84 }
85
86 #[must_use]
88 pub fn encode(&self) -> String {
89 let mut out = format!("? {}", self.code);
90 for arg in &self.args {
91 out.push(' ');
92 out.push_str("e_value(arg));
93 }
94 out.push('\n');
95 out
96 }
97
98 pub fn parse(line: &str) -> Result<Self> {
102 let trimmed = line.trim_end_matches('\n');
103 let body = trimmed
104 .strip_prefix('?')
105 .ok_or(ParseError::InvalidEnvelope {
106 detail: "warning line must start with `?`",
107 })?;
108 let mut tokens = Tokenizer::new(body);
109 let code_tok = tokens.next().ok_or(ParseError::InvalidEnvelope {
110 detail: "warning missing code",
111 })?;
112 let code: WarningCode = code_tok.parse()?;
113 let args: Vec<String> = tokens.map(|t| strip_quotes(t).to_string()).collect();
114 Ok(Self { code, args })
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum Envelope {
121 Success(StateToken),
122 Error { code: ErrorCode, args: Vec<String> },
123}
124
125impl Envelope {
126 #[must_use]
129 pub fn encode(&self) -> String {
130 match self {
131 Self::Success(t) => format!("@{t}\n"),
132 Self::Error { code, args } => {
133 let mut out = format!("! {code}");
134 for arg in args {
135 out.push(' ');
136 out.push_str("e_value(arg));
137 }
138 out.push('\n');
139 out
140 }
141 }
142 }
143
144 pub fn parse(line: &str) -> Result<Self> {
146 let trimmed = line.trim_end_matches('\n');
147 let mut chars = trimmed.chars();
148 match chars.next() {
149 Some('@') => {
150 let rest = chars.as_str();
151 let token: StateToken = rest.parse()?;
152 Ok(Self::Success(token))
153 }
154 Some('!') => {
155 let body = chars.as_str();
156 let mut tokens = Tokenizer::new(body);
157 let code_tok = tokens.next().ok_or(ParseError::InvalidEnvelope {
158 detail: "error missing code",
159 })?;
160 let code: ErrorCode = code_tok.parse()?;
161 let args: Vec<String> = tokens.map(|t| strip_quotes(t).to_string()).collect();
162 Ok(Self::Error { code, args })
163 }
164 _ => Err(ParseError::InvalidEnvelope {
165 detail: "envelope must start with `@` or `!`",
166 }),
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct ResponseHead {
174 pub warnings: Vec<Warning>,
175 pub envelope: Envelope,
176}
177
178impl ResponseHead {
179 #[must_use]
180 pub fn ok(token: StateToken) -> Self {
181 Self {
182 warnings: Vec::new(),
183 envelope: Envelope::Success(token),
184 }
185 }
186
187 #[must_use]
188 pub fn err(code: ErrorCode, args: Vec<String>) -> Self {
189 Self {
190 warnings: Vec::new(),
191 envelope: Envelope::Error { code, args },
192 }
193 }
194
195 #[must_use]
196 pub fn with_warning(mut self, w: Warning) -> Self {
197 self.warnings.push(w);
198 self
199 }
200
201 #[must_use]
205 pub fn encode(&self) -> String {
206 let mut out = String::new();
207 for w in &self.warnings {
208 out.push_str(&w.encode());
209 }
210 out.push_str(&self.envelope.encode());
211 out
212 }
213
214 pub fn parse(input: &str) -> Result<Self> {
217 let mut warnings = Vec::new();
218 for line in input.lines() {
219 if line.trim().is_empty() {
220 continue;
221 }
222 if line.starts_with('?') {
223 warnings.push(Warning::parse(line)?);
224 } else if line.starts_with('@') || line.starts_with('!') {
225 let envelope = Envelope::parse(line)?;
226 return Ok(Self { warnings, envelope });
227 } else {
228 return Err(ParseError::InvalidEnvelope {
229 detail: "expected `?`, `@`, or `!` at start of line",
230 });
231 }
232 }
233 Err(ParseError::InvalidEnvelope {
234 detail: "no envelope line found",
235 })
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn token_round_trip() {
245 let t = StateToken([0xa3, 0xf9, 0xb2, 0xc1, 0xd4, 0xe6, 0xf7, 0x0a]);
246 assert_eq!(t.to_string(), "a3f9b2c1d4e6f70a");
247 assert_eq!("a3f9b2c1d4e6f70a".parse::<StateToken>().unwrap(), t);
248 }
249
250 #[test]
251 fn token_zero() {
252 assert_eq!(StateToken::ZERO.to_string(), "0000000000000000");
253 }
254
255 #[test]
256 fn token_rejects_uppercase() {
257 let err = "A3F9B2C1D4E6F70A".parse::<StateToken>().unwrap_err();
258 matches!(err, ParseError::InvalidToken { .. });
259 }
260
261 #[test]
262 fn token_rejects_short() {
263 let err = "abc".parse::<StateToken>().unwrap_err();
264 matches!(err, ParseError::InvalidToken { .. });
265 }
266
267 #[test]
268 fn warning_round_trip_no_args() {
269 let w = Warning::new(WarningCode::CaptchaVisible);
270 let s = w.encode();
271 assert_eq!(s, "? captcha_visible\n");
272 assert_eq!(Warning::parse(&s).unwrap(), w);
273 }
274
275 #[test]
276 fn warning_round_trip_with_args() {
277 let w = Warning::with_args(WarningCode::Nav, vec!["https://example.com".into()]);
278 let s = w.encode();
279 assert_eq!(s, "? nav https://example.com\n");
280 assert_eq!(Warning::parse(&s).unwrap(), w);
281 }
282
283 #[test]
284 fn warning_arg_with_spaces_quoted() {
285 let w = Warning::with_args(WarningCode::Nav, vec!["with spaces".into()]);
286 let s = w.encode();
287 assert_eq!(s, "? nav \"with spaces\"\n");
288 assert_eq!(Warning::parse(&s).unwrap(), w);
289 }
290
291 #[test]
292 fn envelope_success_round_trip() {
293 let env = Envelope::Success(StateToken([0x12; 8]));
294 let s = env.encode();
295 assert_eq!(s, "@1212121212121212\n");
296 assert_eq!(Envelope::parse(&s).unwrap(), env);
297 }
298
299 #[test]
300 fn envelope_error_round_trip() {
301 let env = Envelope::Error {
302 code: ErrorCode::StaleToken,
303 args: vec!["abcdef0123456789".into(), "nav".into()],
304 };
305 let s = env.encode();
306 assert_eq!(s, "! STALE_TOKEN abcdef0123456789 nav\n");
307 assert_eq!(Envelope::parse(&s).unwrap(), env);
308 }
309
310 #[test]
311 fn response_head_with_warnings_round_trip() {
312 let head = ResponseHead::ok(StateToken([0xab; 8]))
313 .with_warning(Warning::with_args(
314 WarningCode::Nav,
315 vec!["https://example.com".into()],
316 ))
317 .with_warning(Warning::new(WarningCode::CaptchaVisible));
318 let s = head.encode();
319 let parsed = ResponseHead::parse(&s).unwrap();
320 assert_eq!(parsed, head);
321 }
322
323 #[test]
324 fn response_head_error() {
325 let head = ResponseHead::err(ErrorCode::Timeout, vec!["5000ms".into(), "vs_wait".into()]);
326 let s = head.encode();
327 assert_eq!(s, "! TIMEOUT 5000ms vs_wait\n");
328 assert_eq!(ResponseHead::parse(&s).unwrap(), head);
329 }
330}