#![allow(clippy::trivially_copy_pass_by_ref)]
use std::str::FromStr;
use std::time::Duration;
use chrono::NaiveDate;
use nom::branch::alt;
use nom::bytes::complete::{tag, take};
use nom::combinator::{map_res, rest, verify};
use nom::error::{Error, ErrorKind, make_error};
use nom::number::complete::{le_u8, le_u16};
use nom::{Err, IResult, Parser};
use crate::{Emulator, SpcFile, SpcHeader, SpcMemory};
const MAGIC: &[u8] = b"SNES-SPC700 Sound File Data v0.30";
const MAGIC_LENGTH: usize = MAGIC.len();
pub fn parse_from_bytes(bytes: &[u8]) -> Result<SpcFile, Err<Error<&[u8]>>> {
let (_title, (_, header, memory, _rest)) = (tag(MAGIC), header, memory, rest).parse(bytes)?;
Ok(SpcFile { header, memory })
}
fn null_terminated_string(input: &[u8]) -> String {
let mut end = 0;
while end < input.len() && input[end] != 0 {
end += 1;
}
String::from_utf8_lossy(&input[.. end]).into_owned()
}
fn has_id666_info(input: u8) -> IResult<u8, bool> {
match input {
26 => Ok((input, true)),
27 => Ok((input, false)),
_ => Err(Err::Error(make_error(input, ErrorKind::OneOf))),
}
}
#[derive(Debug)]
struct HeaderRest {
pub dump_date: Option<NaiveDate>,
pub duration: Duration,
pub fade_duration: Duration,
pub channel_disables: bool,
pub emulator: Emulator,
pub artist: String,
}
const fn is_day(day: &u8) -> bool {
*day >= 1 && *day <= 31
}
const fn is_month(month: &u8) -> bool {
*month >= 1 && *month <= 12
}
const fn is_year(year: &u16) -> bool {
*year >= 1 && *year <= 9999
}
fn binary_date(input: &[u8]) -> IResult<&[u8], Option<NaiveDate>> {
let (rest, (day, month, year)) = alt((
(verify(le_u8, is_day), verify(le_u8, is_month), verify(le_u16, is_year)),
tag([0; 4].as_ref()).map(|_| (0, 0u8, 0u16)),
))
.parse(input)?;
if day == 0 && month == 0 && year == 0 {
Ok((rest, None))
} else {
NaiveDate::from_ymd_opt(i32::from(year), u32::from(month), u32::from(day))
.map_or_else(|| Err(Err::Error(make_error(input, ErrorKind::Digit))), |v| Ok((rest, Some(v))))
}
}
fn text_date(input: &[u8]) -> IResult<&[u8], Option<NaiveDate>> {
let (rest, (month, day, year)) = alt((
(
take(2usize).and_then(parse_number::<u8>),
tag(b"/".as_ref()),
take(2usize).and_then(parse_number::<u8>),
tag(b"/".as_ref()),
take(4usize).and_then(parse_number::<u16>),
)
.map(|(m, _, d, _, y)| (m, d, y)),
tag([0; 11].as_ref()).map(|_| (0, 0, 0)),
))
.parse(input)?;
if day == 0 && month == 0 && year == 0 {
Ok((rest, None))
} else {
NaiveDate::from_ymd_opt(i32::from(year), u32::from(month), u32::from(day))
.map_or_else(|| Err(Err::Error(make_error(input, ErrorKind::Digit))), |v| Ok((rest, Some(v))))
}
}
fn emulator(input: u8) -> IResult<u8, Emulator> {
match input {
0x00 | 0x30 => Ok((input, Emulator::Unknown)),
0x01 | 0x31 => Ok((input, Emulator::ZSNES)),
0x02 | 0x32 => Ok((input, Emulator::Snes9x)),
0x03 | 0x33 => Ok((input, Emulator::ZST2SPC)),
0x04 | 0x34 => Ok((input, Emulator::Other)),
0x05 | 0x35 => Ok((input, Emulator::SNEShout)),
0x06 | 0x36 => Ok((input, Emulator::ZSNES_W)),
0x07 | 0x37 => Ok((input, Emulator::Snes9xpp)),
0x08 | 0x38 => Ok((input, Emulator::SNESGT)),
_ => Err(Err::Error(make_error(input, ErrorKind::IsNot))),
}
}
fn to_bool(input: u8) -> IResult<u8, bool> {
match input {
0 => Ok((input, false)),
1 => Ok((input, true)),
_ => Err(Err::Error(make_error(input, ErrorKind::IsNot))),
}
}
fn parse_number<T: FromStr>(input: &[u8]) -> IResult<&[u8], T>
where
<T as FromStr>::Err: std::fmt::Debug,
{
null_terminated_string(input)
.parse()
.map_or_else(|_| Err(Err::Error(make_error(input, ErrorKind::Digit))), |v| Ok((input, v)))
}
fn rest_of_binary_header(input: &[u8]) -> IResult<&[u8], HeaderRest> {
let (rest, (dump_date, _, duration, fade_duration, artist, (_, channel_disables), (_, emulator), _)) = (
binary_date,
take(7usize),
take(3usize).and_then(parse_number::<u64>).map(Duration::from_secs),
take(4usize).and_then(parse_number::<u64>).map(Duration::from_millis),
take(32usize).map(null_terminated_string),
map_res(le_u8, to_bool),
map_res(le_u8, emulator),
tag([0; 46].as_ref()),
)
.parse(input)?;
Ok((rest, HeaderRest { dump_date, duration, fade_duration, channel_disables, emulator, artist }))
}
fn rest_of_text_header(input: &[u8]) -> IResult<&[u8], HeaderRest> {
let (rest, (dump_date, duration, fade_duration, artist, (_, channel_disables), (_, emulator), _)) = (
text_date,
take(3usize).and_then(parse_number::<u64>).map(Duration::from_secs),
take(5usize).and_then(parse_number::<u64>).map(Duration::from_millis),
take(32usize).map(null_terminated_string),
map_res(le_u8, to_bool),
map_res(le_u8, emulator),
tag([0; 45].as_ref()),
)
.parse(input)?;
Ok((rest, HeaderRest { dump_date, duration, fade_duration, channel_disables, emulator, artist }))
}
fn header(bytes: &[u8]) -> IResult<&[u8], SpcHeader> {
let (rest, (_, (_, _has_id666), version, pc, a, x, y, psw, sp, _, title, game, dump_author, comments, header_rest)) =
(
tag([26, 26].as_ref()),
map_res(le_u8, has_id666_info),
alt((tag([30].as_ref()), tag([31].as_ref()))).map(|v: &[_]| v[0]),
le_u16,
le_u8,
le_u8,
le_u8,
le_u8,
le_u8,
take(2usize),
take(32usize).map(null_terminated_string),
take(32usize).map(null_terminated_string),
take(16usize).map(null_terminated_string),
take(32usize).map(null_terminated_string),
alt((rest_of_binary_header, rest_of_text_header)),
)
.parse(bytes)?;
let header = SpcHeader {
version,
pc,
a,
x,
y,
psw,
sp,
title,
game,
artist: header_rest.artist,
dump_author,
comments,
dump_date: header_rest.dump_date,
duration: header_rest.duration,
fade_duration: header_rest.fade_duration,
channel_disables: header_rest.channel_disables,
emulator: header_rest.emulator,
};
let header_length = bytes.len() + MAGIC_LENGTH - rest.len();
debug_assert!(header_length == 0x100, "header should be exactly 0x100 in length, but is {header_length:x}");
Ok((rest, header))
}
#[allow(clippy::similar_names)]
fn memory(bytes: &[u8]) -> IResult<&[u8], SpcMemory> {
let (rest, (ram, dsp_registers, _, rom)) =
(take(65536usize), take(128usize), take(64usize), take(64usize)).parse(bytes)?;
Ok((rest, SpcMemory {
ram: Box::new(<[u8; _]>::try_from(ram).unwrap()),
dsp_registers: Box::new(<[u8; _]>::try_from(dsp_registers).unwrap()),
rom: Box::new(<[u8; _]>::try_from(rom).unwrap()),
}))
}