#[derive(Debug, PartialEq)]
pub struct RsyncProgress {
pub bytes_transferred: u64,
#[allow(dead_code)]
pub percent: u8,
#[allow(dead_code)]
pub speed: String,
#[allow(dead_code)]
pub eta: String,
}
pub fn parse_rsync_progress(line: &str) -> Option<RsyncProgress> {
let line = line.trim_end_matches('\r');
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 4 {
return None;
}
let percent_str = fields[1].strip_suffix('%')?;
let percent: u8 = percent_str.parse().ok()?;
let bytes_str = fields[0].replace(',', "");
let bytes_transferred: u64 = bytes_str.parse().ok()?;
Some(RsyncProgress {
bytes_transferred,
percent,
speed: fields[2].to_string(),
eta: fields[3].to_string(),
})
}
const TRANSFERRED_SIZE_PREFIX: &str = "Total transferred file size:";
pub fn parse_transferred_size(stats: &str) -> Option<u64> {
stats
.split(['\r', '\n'])
.find_map(|line| line.trim_start().strip_prefix(TRANSFERRED_SIZE_PREFIX))?
.split_whitespace()
.next()?
.replace(',', "")
.parse()
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_rsync_canonical_line() {
let line = " 1,234,567 42% 12.34MB/s 0:01:23";
let p = parse_rsync_progress(line).unwrap();
assert_eq!(
p,
RsyncProgress {
bytes_transferred: 1234567,
percent: 42,
speed: "12.34MB/s".to_string(),
eta: "0:01:23".to_string(),
}
);
}
#[test]
fn parse_rsync_single_digit_percent() {
let line = " 500 5% 1.00MB/s 0:00:01";
let p = parse_rsync_progress(line).unwrap();
assert_eq!(p.percent, 5);
assert_eq!(p.bytes_transferred, 500);
}
#[test]
fn parse_rsync_100_percent() {
let line = " 10,000,000 100% 50.00MB/s 0:00:00";
let p = parse_rsync_progress(line).unwrap();
assert_eq!(p.percent, 100);
}
#[test]
fn parse_rsync_plain_text_returns_none() {
assert!(parse_rsync_progress("sending incremental file list").is_none());
}
#[test]
fn parse_rsync_too_few_fields_returns_none() {
assert!(parse_rsync_progress("1234 42%").is_none());
}
#[test]
fn parse_rsync_strips_trailing_carriage_return() {
let line = " 1,234,567 42% 12.34MB/s 0:01:23\r";
let p = parse_rsync_progress(line).unwrap();
assert_eq!(p.eta, "0:01:23");
assert_eq!(p.percent, 42);
}
const STATS_BLOCK: &str = "\nNumber of files: 16 (reg: 15, dir: 1)\nTotal file size: 52,428,800 bytes\nTotal transferred file size: 10,485,760 bytes\nLiteral data: 10,485,760 bytes\n";
#[test]
fn parse_transferred_size_reads_comma_grouped_digits() {
assert_eq!(parse_transferred_size(STATS_BLOCK), Some(10_485_760));
}
#[test]
fn parse_transferred_size_ignores_total_file_size() {
let stats = "Total file size: 52,428,800 bytes\n";
assert_eq!(parse_transferred_size(stats), None);
}
#[test]
fn parse_transferred_size_reads_zero() {
let stats = "Total file size: 52,428,800 bytes\nTotal transferred file size: 0 bytes\n";
assert_eq!(parse_transferred_size(stats), Some(0));
}
#[test]
fn parse_transferred_size_absent_line_returns_none() {
assert_eq!(
parse_transferred_size("sending incremental file list\n"),
None
);
}
#[test]
fn parse_transferred_size_tolerates_leading_whitespace() {
let stats = " Total transferred file size: 4,096 bytes\n";
assert_eq!(parse_transferred_size(stats), Some(4096));
}
#[test]
fn parse_transferred_size_survives_a_progress_redraw_on_the_same_line() {
let stats = " 10,485,760 100% 1.00MB/s 0:00:10\rTotal transferred file size: 10,485,760 bytes\n";
assert_eq!(parse_transferred_size(stats), Some(10_485_760));
}
#[test]
fn parse_transferred_size_rejects_non_numeric_value() {
assert_eq!(
parse_transferred_size("Total transferred file size: N/A\n"),
None
);
}
}