1use std::pin::Pin;
2use std::task::{Context, Poll};
3use tokio::io::{AsyncBufRead, AsyncRead, BufReader, ReadBuf};
4
5use crate::Result;
6use crate::event::{CrocEvent, FileInfo, Progress};
7
8pub struct CrocParser<R: AsyncBufRead + Unpin> {
10 reader: BufReader<R>,
12 is_receiving: bool,
14 buf: Vec<u8>,
16 last_line: String,
18}
19
20impl<R: AsyncBufRead + Unpin> CrocParser<R> {
21 pub fn new(inner: R) -> Self {
23 let reader = BufReader::new(inner);
24 Self {
25 reader,
26 is_receiving: false,
27 buf: Vec::new(),
28 last_line: String::new(),
29 }
30 }
31
32 pub fn poll_next_event(&mut self, cx: &mut Context<'_>) -> Poll<Result<CrocEvent>> {
34 loop {
35 let mut byte = [0u8; 1];
36 let mut read_buf = ReadBuf::new(&mut byte);
37 match Pin::new(&mut self.reader).poll_read(cx, &mut read_buf) {
38 Poll::Ready(Ok(())) => {
39 if read_buf.filled().is_empty() {
40 if !self.buf.is_empty() {
41 let raw_line = String::from_utf8_lossy(&self.buf).to_string();
42 self.buf.clear();
43 let line = Self::strip_ansi(&raw_line);
44 if !line.trim().is_empty() && line != self.last_line {
45 self.last_line = line.clone();
46 if let Some(event) = self.parse_line(&line) {
47 return Poll::Ready(Ok(event));
48 }
49 }
50 }
51 if self.last_line != "DONE" {
52 self.last_line = "DONE".to_string();
53 return Poll::Ready(Ok(CrocEvent::Done));
54 }
55 return Poll::Ready(Ok(CrocEvent::EOF));
56 }
57 let b = byte[0];
58 if b == b'\r' || b == b'\n' {
59 if self.buf.is_empty() {
60 continue;
61 }
62 let raw_line = String::from_utf8_lossy(&self.buf).to_string();
63 self.buf.clear();
64 let line = Self::strip_ansi(&raw_line);
65 if line.trim().is_empty() || line == self.last_line {
66 continue;
67 }
68 self.last_line = line.clone();
69 if let Some(event) = self.parse_line(&line) {
70 return Poll::Ready(Ok(event));
71 }
72 } else {
73 self.buf.push(b);
74 }
75 }
76 Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())),
77 Poll::Pending => return Poll::Pending,
78 }
79 }
80 }
81
82 fn parse_line(&mut self, line: &str) -> Option<CrocEvent> {
84 let line = line.trim();
85
86 if Self::should_ignore(line) {
87 return None;
88 }
89
90 if let Some(event) = self.parse_hashing(line) {
91 return Some(event);
92 }
93
94 if let Some(event) = self.parse_info(line) {
95 return Some(event);
96 }
97
98 if let Some(event) = self.parse_code(line) {
99 return Some(event);
100 }
101
102 if let Some(event) = self.parse_state_change(line) {
103 return Some(event);
104 }
105
106 if let Some(event) = self.parse_progress(line) {
107 return Some(event);
108 }
109
110 Some(CrocEvent::Unknown(line.to_string()))
111 }
112
113 fn should_ignore(line: &str) -> bool {
115 line == "On the other computer run:"
116 || line == "(For Windows)"
117 || line == "(For Linux/macOS)"
118 || line.starts_with("croc ")
119 || line.starts_with("CROC_SECRET=")
120 }
121
122 fn parse_hashing(&self, line: &str) -> Option<CrocEvent> {
124 if !line.starts_with("Hashing ") {
125 return None;
126 }
127
128 let percent_idx = line.find("% |")?;
129 let left = line[8..percent_idx].trim();
130 let last_space = left.rfind(' ')?;
131
132 let file_name = left[..last_space].trim().to_string();
133 let percent_str = left[last_space..].trim();
134 let percentage = percent_str.parse::<u8>().ok()?;
135
136 let (bytes_sent, bytes_total, speed) = Self::parse_metadata(line);
137
138 Some(CrocEvent::Hashing(Progress {
139 file_name,
140 percentage,
141 bytes_sent,
142 bytes_total,
143 speed,
144 }))
145 }
146
147 fn parse_info(&mut self, line: &str) -> Option<CrocEvent> {
149 if line.starts_with("Sending '") {
150 let end_quote = line[9..].find('\'')?;
151 let end_quote_idx = 9 + end_quote;
152 let name = line[9..end_quote_idx].to_string();
153
154 let start_paren = line[end_quote_idx..].find('(')?;
155 let start_paren_idx = end_quote_idx + start_paren + 1;
156 let end_paren = line[start_paren_idx..].find(')')?;
157
158 let size_str = line[start_paren_idx..start_paren_idx + end_paren].to_string();
159 let size = Self::parse_bytes(&size_str).unwrap_or(0);
160 self.is_receiving = false;
161 return Some(CrocEvent::SendingInfo(FileInfo { name, size }));
162 }
163
164 if line.starts_with("Receiving '") {
165 let end_quote = line[11..].find('\'')?;
166 let end_quote_idx = 11 + end_quote;
167 let name = line[11..end_quote_idx].to_string();
168
169 let start_paren = line[end_quote_idx..].find('(')?;
170 let start_paren_idx = end_quote_idx + start_paren + 1;
171 let end_paren = line[start_paren_idx..].find(')')?;
172
173 let size_str = line[start_paren_idx..start_paren_idx + end_paren].to_string();
174 let size = Self::parse_bytes(&size_str).unwrap_or(0);
175 self.is_receiving = true;
176 return Some(CrocEvent::ReceivingInfo(FileInfo { name, size }));
177 }
178
179 None
180 }
181
182 fn parse_code(&self, line: &str) -> Option<CrocEvent> {
184 if line.starts_with("Code is: ") {
185 let code = line[9..].trim().to_string();
186 return Some(CrocEvent::CodeGenerated(code));
187 }
188 None
189 }
190
191 fn parse_state_change(&mut self, line: &str) -> Option<CrocEvent> {
193 if line.starts_with("Receiving (") {
194 self.is_receiving = true;
195 if let Some(relay) = Self::parse_relay_address(line, "<-") {
196 return Some(CrocEvent::ReceivingFrom(relay));
197 }
198 return Some(CrocEvent::Unknown(line.to_string()));
199 } else if line.starts_with("Sending (") {
200 self.is_receiving = false;
201 if let Some(relay) = Self::parse_relay_address(line, "->") {
202 return Some(CrocEvent::SendingTo(relay));
203 }
204 return Some(CrocEvent::Unknown(line.to_string()));
205 }
206 None
207 }
208
209 fn parse_relay_address(line: &str, delimiter: &str) -> Option<crate::croc::Relay> {
211 let start = line.find(delimiter)?;
212 let end = line.rfind(')')?;
213 let ip_port = &line[start + 2..end];
214 let addr = ip_port.parse::<std::net::SocketAddr>().ok()?;
215 Some(crate::croc::Relay::new(addr.ip(), addr.port()))
216 }
217
218 fn parse_progress(&self, line: &str) -> Option<CrocEvent> {
220 let percent_idx = line.find("% |")?;
221 let left = line[..percent_idx].trim();
222 let last_space = left.rfind(' ')?;
223
224 let file_name = left[..last_space].trim().to_string();
225 let percent_str = left[last_space..].trim();
226 let percentage = percent_str.parse::<u8>().ok()?;
227
228 let (bytes_sent, bytes_total, speed) = Self::parse_metadata(line);
229
230 let progress = Progress {
231 file_name,
232 percentage,
233 bytes_sent,
234 bytes_total,
235 speed,
236 };
237
238 if self.is_receiving {
239 Some(CrocEvent::Receiving(progress))
240 } else {
241 Some(CrocEvent::Sending(progress))
242 }
243 }
244
245 fn parse_metadata(line: &str) -> (Option<u64>, Option<u64>, Option<f64>) {
247 let paren_idx = match line.rfind('(') {
248 Some(idx) => idx,
249 None => return (None, None, None),
250 };
251
252 let end_paren = line[paren_idx..]
253 .find(')')
254 .unwrap_or(line.len() - paren_idx);
255 let meta_str = line[paren_idx + 1..paren_idx + end_paren].trim();
256 let parts: Vec<&str> = meta_str.splitn(2, ',').collect();
257
258 if parts.len() == 1 && (parts[0].trim().ends_with("/s") || parts[0].trim().ends_with("ps"))
259 {
260 return (None, None, Self::parse_speed(parts[0].trim()));
261 }
262
263 let (bytes_sent, bytes_total, mut speed) = Self::parse_bytes_part(parts[0].trim());
264
265 if parts.len() > 1 {
266 speed = Self::parse_speed(parts[1].trim());
267 }
268
269 (bytes_sent, bytes_total, speed)
270 }
271
272 fn parse_bytes_part(bytes_part: &str) -> (Option<u64>, Option<u64>, Option<f64>) {
274 if let Some(slash_idx) = bytes_part.find('/') {
275 let sent_str = bytes_part[..slash_idx].trim();
276 let total_str = bytes_part[slash_idx + 1..].trim();
277
278 if total_str.starts_with('s') {
279 (None, None, Self::parse_speed(bytes_part))
280 } else {
281 let (sent, total) = Self::parse_fractional_bytes(sent_str, total_str);
282 (sent, total, None)
283 }
284 } else {
285 (Self::parse_bytes(bytes_part), None, None)
286 }
287 }
288
289 fn parse_fractional_bytes(sent_str: &str, total_str: &str) -> (Option<u64>, Option<u64>) {
291 let total_unit = if let Some(idx) = total_str.find(|c: char| c.is_alphabetic()) {
292 &total_str[idx..]
293 } else {
294 ""
295 };
296
297 let sent_has_unit = sent_str.contains(|c: char| c.is_alphabetic());
298 let sent_to_parse = if !sent_has_unit && !total_unit.is_empty() {
299 format!("{} {}", sent_str, total_unit)
300 } else {
301 sent_str.to_string()
302 };
303
304 (
305 Self::parse_bytes(&sent_to_parse),
306 Self::parse_bytes(total_str),
307 )
308 }
309
310 fn parse_unit_multiplier(unit: &str) -> f64 {
312 match unit.trim().to_uppercase().as_str() {
313 "B" | "" => 1.0,
314 "KB" | "K" => 1024.0,
315 "MB" | "M" => 1024.0 * 1024.0,
316 "GB" | "G" => 1024.0 * 1024.0 * 1024.0,
317 "TB" | "T" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
318 _ => 1.0,
319 }
320 }
321
322 fn strip_ansi(s: &str) -> String {
324 let mut result = String::with_capacity(s.len());
325 let mut in_ansi = false;
326 for c in s.chars() {
327 if c == '\x1b' {
328 in_ansi = true;
329 } else if in_ansi {
330 if c.is_ascii_alphabetic() {
331 in_ansi = false;
332 }
333 } else {
334 result.push(c);
335 }
336 }
337 result
338 }
339
340 fn parse_bytes(s: &str) -> Option<u64> {
342 let s = s.trim();
343 let (num_str, unit) = if let Some(idx) = s.find(|c: char| c.is_alphabetic()) {
344 (&s[..idx], &s[idx..])
345 } else {
346 (s, "")
347 };
348
349 let val: f64 = num_str.trim().parse().ok()?;
350 let multiplier = Self::parse_unit_multiplier(unit);
351 Some((val * multiplier) as u64)
352 }
353
354 fn parse_speed(s: &str) -> Option<f64> {
356 let s = s.trim().trim_end_matches("/s").trim_end_matches("ps");
357 let (num_str, unit) = if let Some(idx) = s.find(|c: char| c.is_alphabetic()) {
358 (&s[..idx], &s[idx..])
359 } else {
360 (s, "")
361 };
362
363 let val: f64 = num_str.trim().parse().ok()?;
364 let multiplier = Self::parse_unit_multiplier(unit);
365 Some(val * multiplier)
366 }
367}