272 lines
7.5 KiB
Rust
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());
|
|
}
|
|
}
|