use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncBufRead, AsyncRead, BufReader, ReadBuf};
use crate::Result;
use crate::event::{CrocEvent, FileInfo, Progress};
pub struct CrocParser<R: AsyncBufRead + Unpin> {
reader: BufReader<R>,
is_receiving: bool,
buf: Vec<u8>,
last_line: String,
}
impl<R: AsyncBufRead + Unpin> CrocParser<R> {
pub fn new(inner: R) -> Self {
let reader = BufReader::new(inner);
Self {
reader,
is_receiving: false,
buf: Vec::new(),
last_line: String::new(),
}
}
pub fn poll_next_event(&mut self, cx: &mut Context<'_>) -> Poll<Result<CrocEvent>> {
loop {
let mut byte = [0u8; 1];
let mut read_buf = ReadBuf::new(&mut byte);
match Pin::new(&mut self.reader).poll_read(cx, &mut read_buf) {
Poll::Ready(Ok(())) => {
if read_buf.filled().is_empty() {
if !self.buf.is_empty() {
let raw_line = String::from_utf8_lossy(&self.buf).to_string();
self.buf.clear();
let line = Self::strip_ansi(&raw_line);
if !line.trim().is_empty() && line != self.last_line {
self.last_line = line.clone();
if let Some(event) = self.parse_line(&line) {
return Poll::Ready(Ok(event));
}
}
}
if self.last_line != "DONE" {
self.last_line = "DONE".to_string();
return Poll::Ready(Ok(CrocEvent::Done));
}
return Poll::Ready(Ok(CrocEvent::EOF));
}
let b = byte[0];
if b == b'\r' || b == b'\n' {
if self.buf.is_empty() {
continue;
}
let raw_line = String::from_utf8_lossy(&self.buf).to_string();
self.buf.clear();
let line = Self::strip_ansi(&raw_line);
if line.trim().is_empty() || line == self.last_line {
continue;
}
self.last_line = line.clone();
if let Some(event) = self.parse_line(&line) {
return Poll::Ready(Ok(event));
}
} else {
self.buf.push(b);
}
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())),
Poll::Pending => return Poll::Pending,
}
}
}
fn parse_line(&mut self, line: &str) -> Option<CrocEvent> {
let line = line.trim();
if Self::should_ignore(line) {
return None;
}
if let Some(event) = self.parse_hashing(line) {
return Some(event);
}
if let Some(event) = self.parse_info(line) {
return Some(event);
}
if let Some(event) = self.parse_code(line) {
return Some(event);
}
if let Some(event) = self.parse_state_change(line) {
return Some(event);
}
if let Some(event) = self.parse_progress(line) {
return Some(event);
}
Some(CrocEvent::Unknown(line.to_string()))
}
fn should_ignore(line: &str) -> bool {
line == "On the other computer run:"
|| line == "(For Windows)"
|| line == "(For Linux/macOS)"
|| line.starts_with("croc ")
|| line.starts_with("CROC_SECRET=")
}
fn parse_hashing(&self, line: &str) -> Option<CrocEvent> {
if !line.starts_with("Hashing ") {
return None;
}
let percent_idx = line.find("% |")?;
let left = line[8..percent_idx].trim();
let last_space = left.rfind(' ')?;
let file_name = left[..last_space].trim().to_string();
let percent_str = left[last_space..].trim();
let percentage = percent_str.parse::<u8>().ok()?;
let (bytes_sent, bytes_total, speed) = Self::parse_metadata(line);
Some(CrocEvent::Hashing(Progress {
file_name,
percentage,
bytes_sent,
bytes_total,
speed,
}))
}
fn parse_info(&mut self, line: &str) -> Option<CrocEvent> {
if line.starts_with("Sending '") {
let end_quote = line[9..].find('\'')?;
let end_quote_idx = 9 + end_quote;
let name = line[9..end_quote_idx].to_string();
let start_paren = line[end_quote_idx..].find('(')?;
let start_paren_idx = end_quote_idx + start_paren + 1;
let end_paren = line[start_paren_idx..].find(')')?;
let size_str = line[start_paren_idx..start_paren_idx + end_paren].to_string();
let size = Self::parse_bytes(&size_str).unwrap_or(0);
self.is_receiving = false;
return Some(CrocEvent::SendingInfo(FileInfo { name, size }));
}
if line.starts_with("Receiving '") {
let end_quote = line[11..].find('\'')?;
let end_quote_idx = 11 + end_quote;
let name = line[11..end_quote_idx].to_string();
let start_paren = line[end_quote_idx..].find('(')?;
let start_paren_idx = end_quote_idx + start_paren + 1;
let end_paren = line[start_paren_idx..].find(')')?;
let size_str = line[start_paren_idx..start_paren_idx + end_paren].to_string();
let size = Self::parse_bytes(&size_str).unwrap_or(0);
self.is_receiving = true;
return Some(CrocEvent::ReceivingInfo(FileInfo { name, size }));
}
None
}
fn parse_code(&self, line: &str) -> Option<CrocEvent> {
if line.starts_with("Code is: ") {
let code = line[9..].trim().to_string();
return Some(CrocEvent::CodeGenerated(code));
}
None
}
fn parse_state_change(&mut self, line: &str) -> Option<CrocEvent> {
if line.starts_with("Receiving (") {
self.is_receiving = true;
if let Some(relay) = Self::parse_relay_address(line, "<-") {
return Some(CrocEvent::ReceivingFrom(relay));
}
return Some(CrocEvent::Unknown(line.to_string()));
} else if line.starts_with("Sending (") {
self.is_receiving = false;
if let Some(relay) = Self::parse_relay_address(line, "->") {
return Some(CrocEvent::SendingTo(relay));
}
return Some(CrocEvent::Unknown(line.to_string()));
}
None
}
fn parse_relay_address(line: &str, delimiter: &str) -> Option<crate::croc::Relay> {
let start = line.find(delimiter)?;
let end = line.rfind(')')?;
let ip_port = &line[start + 2..end];
let addr = ip_port.parse::<std::net::SocketAddr>().ok()?;
Some(crate::croc::Relay::new(addr.ip(), addr.port()))
}
fn parse_progress(&self, line: &str) -> Option<CrocEvent> {
let percent_idx = line.find("% |")?;
let left = line[..percent_idx].trim();
let last_space = left.rfind(' ')?;
let file_name = left[..last_space].trim().to_string();
let percent_str = left[last_space..].trim();
let percentage = percent_str.parse::<u8>().ok()?;
let (bytes_sent, bytes_total, speed) = Self::parse_metadata(line);
let progress = Progress {
file_name,
percentage,
bytes_sent,
bytes_total,
speed,
};
if self.is_receiving {
Some(CrocEvent::Receiving(progress))
} else {
Some(CrocEvent::Sending(progress))
}
}
fn parse_metadata(line: &str) -> (Option<u64>, Option<u64>, Option<f64>) {
let paren_idx = match line.rfind('(') {
Some(idx) => idx,
None => return (None, None, None),
};
let end_paren = line[paren_idx..]
.find(')')
.unwrap_or(line.len() - paren_idx);
let meta_str = line[paren_idx + 1..paren_idx + end_paren].trim();
let parts: Vec<&str> = meta_str.splitn(2, ',').collect();
if parts.len() == 1 && (parts[0].trim().ends_with("/s") || parts[0].trim().ends_with("ps"))
{
return (None, None, Self::parse_speed(parts[0].trim()));
}
let (bytes_sent, bytes_total, mut speed) = Self::parse_bytes_part(parts[0].trim());
if parts.len() > 1 {
speed = Self::parse_speed(parts[1].trim());
}
(bytes_sent, bytes_total, speed)
}
fn parse_bytes_part(bytes_part: &str) -> (Option<u64>, Option<u64>, Option<f64>) {
if let Some(slash_idx) = bytes_part.find('/') {
let sent_str = bytes_part[..slash_idx].trim();
let total_str = bytes_part[slash_idx + 1..].trim();
if total_str.starts_with('s') {
(None, None, Self::parse_speed(bytes_part))
} else {
let (sent, total) = Self::parse_fractional_bytes(sent_str, total_str);
(sent, total, None)
}
} else {
(Self::parse_bytes(bytes_part), None, None)
}
}
fn parse_fractional_bytes(sent_str: &str, total_str: &str) -> (Option<u64>, Option<u64>) {
let total_unit = if let Some(idx) = total_str.find(|c: char| c.is_alphabetic()) {
&total_str[idx..]
} else {
""
};
let sent_has_unit = sent_str.contains(|c: char| c.is_alphabetic());
let sent_to_parse = if !sent_has_unit && !total_unit.is_empty() {
format!("{} {}", sent_str, total_unit)
} else {
sent_str.to_string()
};
(
Self::parse_bytes(&sent_to_parse),
Self::parse_bytes(total_str),
)
}
fn parse_unit_multiplier(unit: &str) -> f64 {
match unit.trim().to_uppercase().as_str() {
"B" | "" => 1.0,
"KB" | "K" => 1024.0,
"MB" | "M" => 1024.0 * 1024.0,
"GB" | "G" => 1024.0 * 1024.0 * 1024.0,
"TB" | "T" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
_ => 1.0,
}
}
fn strip_ansi(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut in_ansi = false;
for c in s.chars() {
if c == '\x1b' {
in_ansi = true;
} else if in_ansi {
if c.is_ascii_alphabetic() {
in_ansi = false;
}
} else {
result.push(c);
}
}
result
}
fn parse_bytes(s: &str) -> Option<u64> {
let s = s.trim();
let (num_str, unit) = if let Some(idx) = s.find(|c: char| c.is_alphabetic()) {
(&s[..idx], &s[idx..])
} else {
(s, "")
};
let val: f64 = num_str.trim().parse().ok()?;
let multiplier = Self::parse_unit_multiplier(unit);
Some((val * multiplier) as u64)
}
fn parse_speed(s: &str) -> Option<f64> {
let s = s.trim().trim_end_matches("/s").trim_end_matches("ps");
let (num_str, unit) = if let Some(idx) = s.find(|c: char| c.is_alphabetic()) {
(&s[..idx], &s[idx..])
} else {
(s, "")
};
let val: f64 = num_str.trim().parse().ok()?;
let multiplier = Self::parse_unit_multiplier(unit);
Some(val * multiplier)
}
}