use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref PERCENT_PATTERN: Regex = Regex::new(r"(\d+(?:\.\d+)?)\s*%").unwrap();
static ref FRACTION_PATTERN: Regex = Regex::new(r"(\d+)\s*/\s*(\d+)").unwrap();
static ref APT_PROGRESS: Regex = Regex::new(r"(?:Reading|Building|Preparing|Unpacking|Setting up|Processing).*?(\d+)%").unwrap();
}
pub fn parse_progress(text: &str) -> Option<f32> {
const MAX_LINE_LENGTH: usize = 1000;
if text.len() > MAX_LINE_LENGTH {
return None;
}
if let Some(cap) = APT_PROGRESS.captures(text)
&& let Ok(percent) = cap[1].parse::<f32>()
{
return Some(percent.min(100.0));
}
if let Some(cap) = PERCENT_PATTERN.captures(text)
&& let Ok(percent) = cap[1].parse::<f32>()
{
return Some(percent.min(100.0));
}
if let Some(cap) = FRACTION_PATTERN.captures(text)
&& let (Ok(current), Ok(total)) = (cap[1].parse::<f32>(), cap[2].parse::<f32>())
&& total > 0.0
{
return Some((current / total * 100.0).min(100.0));
}
None
}
pub fn parse_progress_from_output(output: &[u8]) -> Option<f32> {
let text = String::from_utf8_lossy(output);
text.lines()
.rev()
.take(20)
.filter_map(parse_progress)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
pub fn extract_status_message(output: &[u8]) -> Option<String> {
let text = String::from_utf8_lossy(output);
text.lines()
.rev()
.take(10)
.find(|line| {
let line = line.trim();
!line.is_empty() && line.len() < 100 })
.map(|line| {
let line = line.trim();
if line.len() > 80 {
format!("{}...", &line[..77])
} else {
line.to_string()
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_percent() {
assert_eq!(parse_progress("Progress: 78%"), Some(78.0));
assert_eq!(parse_progress("100%"), Some(100.0));
assert_eq!(parse_progress("50.5%"), Some(50.5));
assert_eq!(parse_progress(" 25 % "), Some(25.0));
}
#[test]
fn test_parse_fraction() {
assert_eq!(parse_progress("23/100"), Some(23.0));
assert_eq!(parse_progress("45 / 50"), Some(90.0));
assert_eq!(parse_progress("1/2"), Some(50.0));
}
#[test]
fn test_parse_apt_progress() {
assert_eq!(parse_progress("Reading package lists... 78%"), Some(78.0));
assert_eq!(
parse_progress("Building dependency tree... 50%"),
Some(50.0)
);
assert_eq!(parse_progress("Unpacking postgresql... 95%"), Some(95.0));
}
#[test]
fn test_parse_no_progress() {
assert_eq!(parse_progress("No progress here"), None);
assert_eq!(parse_progress("Starting..."), None);
assert_eq!(parse_progress(""), None);
}
#[test]
fn test_parse_progress_from_output() {
let output = b"Starting...\nDownloading: 50%\nDownloading: 75%\nDone";
assert_eq!(parse_progress_from_output(output), Some(75.0));
let no_progress = b"Just some text\nNo progress here";
assert_eq!(parse_progress_from_output(no_progress), None);
}
#[test]
fn test_parse_multiple_progress() {
let output = b"Step 1: 25%\nStep 2: 50%\nStep 3: 75%\nStep 4: 60%";
assert_eq!(parse_progress_from_output(output), Some(75.0));
}
#[test]
fn test_extract_status_message() {
let output = b"Downloading packages...\nUnpacking postgresql-14...";
assert_eq!(
extract_status_message(output),
Some("Unpacking postgresql-14...".to_string())
);
let empty = b"";
assert_eq!(extract_status_message(empty), None);
}
}