//! Span argument parsing, a direct port of `parse_offset()` from main.c. use cdrustanoia_core::device::DiscToc; #[derive(Debug, Clone)] pub struct SpanError(pub String); impl std::fmt::Display for SpanError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } impl std::error::Error for SpanError {} fn err(msg: impl Into) -> SpanError { SpanError(msg.into()) } #[derive(Default, Debug, Clone, Copy)] struct TimeParts { hours: Option, minutes: Option, seconds: Option, sectors: Option, } fn parse_num(s: &str, original: &str) -> Result { let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect(); if digits.is_empty() { Ok(0) } else { digits .parse::() .map_err(|_| err(format!("Error parsing span argument: {original}"))) } } fn parse_time(time: &str) -> Result { let mut parts = TimeParts::default(); let mut rest = time; if let Some(pos) = rest.rfind('.') { parts.sectors = Some(parse_num(&rest[pos + 1..], time)?); rest = &rest[..pos]; if rest.contains('.') { return Err(err("Error parsing span argument")); } } let fields: Vec<&str> = rest.split(':').collect(); if fields.len() > 3 { return Err(err("Error parsing span argument")); } // The original walks right-to-left: rightmost field is seconds, then // minutes, then hours. match fields.len() { 1 => { parts.seconds = Some(parse_num(fields[0], time)?); } 2 => { parts.seconds = Some(parse_num(fields[1], time)?); parts.minutes = Some(parse_num(fields[0], time)?); } 3 => { parts.seconds = Some(parse_num(fields[2], time)?); parts.minutes = Some(parse_num(fields[1], time)?); parts.hours = Some(parse_num(fields[0], time)?); } _ => return Err(err("Error parsing span argument")), } Ok(parts) } fn has_time(parts: &TimeParts) -> bool { parts.seconds.is_some() || parts.sectors.is_some() } /// Port of `parse_offset`. `begin` is the beginning of a span, if known. pub fn parse_offset( toc: &DiscToc, offset: Option<&str>, begin: Option, ) -> Result { let offset = match offset { Some(s) => s, None => return Ok(-1), }; let (track_text, time_text) = if let Some(close) = offset.find(']') { let before = &offset[..close]; if let Some(open) = before.find('[') { (&before[..open], Some(&before[open + 1..])) } else { return Err(err("Error parsing span argument")); } } else { (offset, None) }; let digit_count = track_text .chars() .take_while(|c| c.is_ascii_digit()) .count(); let track = if digit_count > 0 { let t = track_text[..digit_count] .parse::() .map_err(|_| err("Error parsing span argument"))?; if t < 0 || t > toc.tracks as i64 { return Err(err(format!("Track #{t} does not exist."))); } Some(t) } else { None }; let parts = match time_text { Some(t) if !t.is_empty() => parse_time(t)?, Some(_) => TimeParts::default(), None => TimeParts::default(), }; let ret = match track { None => { if !has_time(&parts) { return Ok(-1); } begin.unwrap_or_else(|| toc.disc_firstsector()) } Some(t) => { if !has_time(&parts) { if begin.is_none() { return Ok(toc.track_firstsector(t as i32)); } return Ok(toc.track_lastsector(t as i32)); } toc.track_firstsector(t as i32) } }; let mut ret = ret; let mut add = |v: i64, scale: i64| -> Result<(), SpanError> { ret = ret .checked_add( v.checked_mul(scale) .ok_or_else(|| err("Error parsing span argument"))?, ) .ok_or_else(|| err("Error parsing span argument"))?; Ok(()) }; if let Some(v) = parts.sectors { add(v, 1)?; } if let Some(v) = parts.seconds { add(v, 75)?; } if let Some(v) = parts.minutes { add(v, 60 * 75)?; } if let Some(v) = parts.hours { add(v, 60 * 60 * 75)?; } if let Some(t) = track { if toc.sector_gettrack(ret) != t as i32 { return Err(err( "Time/sector offset goes beyond end of specified track.", )); } } if ret > toc.disc_lastsector() { return Err(err("Time/sector offset goes beyond end of disc.")); } Ok(ret) } /// Parse a complete span expression, returning inclusive first/last sectors. pub fn parse_span(toc: &DiscToc, span: &str) -> Result<(i64, i64), SpanError> { let first_hyphen = span.find('-'); let last_hyphen = span.rfind('-'); if first_hyphen != last_hyphen { return Err(err("Error parsing span argument")); } let (part1, part2, had_hyphen) = match first_hyphen { Some(pos) => (&span[..pos], Some(&span[pos + 1..]), true), None => (span, None, false), }; let mut first = parse_offset(toc, Some(part1), None)?; let mut last = if first == -1 { parse_offset(toc, part2, Some(toc.disc_firstsector()))? } else { parse_offset(toc, part2, Some(first))? }; if first == -1 { if last == -1 { return Err(err("Error parsing span argument")); } first = toc.disc_firstsector(); } else if last == -1 { if had_hyphen { last = toc.disc_lastsector(); } else { let track = toc.sector_gettrack(first); last = toc.track_lastsector(track); } } Ok((first, last)) } #[cfg(test)] mod tests { use super::*; use cdrustanoia_core::device::TocEntry; fn toc() -> DiscToc { let mut t = DiscToc::default(); t.entries[0] = TocEntry { flags: 0, track: 1, start_sector: 0, }; t.entries[1] = TocEntry { flags: 0, track: 2, start_sector: 75000, }; t.entries[2] = TocEntry { flags: 0, track: 3, start_sector: 150000, }; t.entries[3] = TocEntry { flags: 0x04, track: 0xAA, start_sector: 225000, }; t.tracks = 3; t } #[test] fn simple_track() { assert_eq!(parse_span(&toc(), "2").unwrap(), (75000, 149999)); } #[test] fn ranges() { assert_eq!(parse_span(&toc(), "2-3").unwrap(), (75000, 224999)); assert_eq!(parse_span(&toc(), "-2").unwrap(), (0, 149999)); assert_eq!(parse_span(&toc(), "2-").unwrap(), (75000, 224999)); } #[test] fn times() { assert_eq!(parse_span(&toc(), "1[1:00]").unwrap(), (4500, 74999)); assert_eq!(parse_span(&toc(), "2[::20]").unwrap(), (76500, 149999)); assert_eq!(parse_span(&toc(), "2[.30]").unwrap(), (75030, 149999)); assert_eq!(parse_span(&toc(), "2[10:]").unwrap(), (120000, 149999)); } #[test] fn bad() { assert!(parse_span(&toc(), "4").is_err()); assert!(parse_span(&toc(), "1-2-3").is_err()); assert!(parse_span(&toc(), "2[1:00:00:00]").is_err()); } }