Files
cdrustanoia/crates/cli/src/span.rs
T
2026-11-03 20:19:21 +01:00

272 lines
7.5 KiB
Rust

//! 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<String>) -> SpanError {
SpanError(msg.into())
}
#[derive(Default, Debug, Clone, Copy)]
struct TimeParts {
hours: Option<i64>,
minutes: Option<i64>,
seconds: Option<i64>,
sectors: Option<i64>,
}
fn parse_num(s: &str, original: &str) -> Result<i64, SpanError> {
let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
Ok(0)
} else {
digits
.parse::<i64>()
.map_err(|_| err(format!("Error parsing span argument: {original}")))
}
}
fn parse_time(time: &str) -> Result<TimeParts, SpanError> {
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<i64>,
) -> Result<i64, SpanError> {
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::<i64>()
.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());
}
}