first draft
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user