use std::fmt::Debug;
use super::utils::ByteIterator;
use crate::{read, utils::ByteString};
#[derive(Debug, Clone, Default)]
pub struct MsdFile {
pub elements: Vec<MsdElement>,
}
impl MsdFile {
pub fn new() -> Self {
Self::default()
}
pub fn with_tag(&self, tag: &str) -> Option<MsdElement> {
let tag_upper = tag.to_ascii_uppercase();
let tag_upper = tag_upper.as_bytes();
for el in &self.elements {
if *el.tag == *tag_upper {
return Some(el.clone());
}
}
None
}
pub fn first_tag_first_val(&self, tag: &str) -> Option<Box<[u8]>> {
let v = self.with_tag(tag)?;
match v.values.first() {
Some(v) => {
if v.is_empty() {
return None;
}
Some(v.clone())
}
None => None,
}
}
pub fn all_with_tag(&self, tag: &str) -> Vec<&MsdElement> {
let tag_upper = tag.to_ascii_uppercase();
let tag_upper = tag_upper.as_bytes();
self.elements
.iter()
.filter(|f| *f.tag == *tag_upper)
.collect()
}
}
#[derive(Clone)]
pub struct MsdElement {
pub tag: ByteString,
pub values: Vec<ByteString>,
}
impl Debug for MsdElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MsdElement")
.field("tag", &String::from_utf8_lossy(&self.tag))
.field(
"values",
&self
.values
.iter()
.map(|f| String::from_utf8_lossy(f))
.collect::<Vec<_>>(),
)
.finish()
}
}
fn write_param(data: &mut Vec<u8>, param: &ByteString) {
for byte in param.iter() {
let byte = *byte;
if matches!(byte, b':' | b';' | b'#' | b'\\') {
data.push(b'\\');
}
data.push(byte);
}
}
pub fn serialize_msd_elements(elements: Vec<MsdElement>) -> Box<[u8]> {
let mut data = vec![];
for el in elements {
data.push(b'#');
write_param(&mut data, &el.tag);
for value in el.values {
data.push(b':');
write_param(&mut data, &value);
}
data.push(b';');
data.push(b'\n');
}
data.into()
}
pub fn from_bytes(buf: &[u8]) -> MsdFile {
let mut elements: Vec<MsdElement> = vec![];
let mut b_iter = ByteIterator::new(buf);
loop {
let (param_list, term_reason) = parse_param_list(&mut b_iter);
if let Some(tag) = param_list.first() {
elements.push(MsdElement {
tag: tag.to_ascii_uppercase().into(),
values: param_list.into_iter().skip(1).collect(),
})
}
if matches!(term_reason, ParamListTermReason::Eof) {
break;
}
}
MsdFile { elements }
}
enum ParamListTermReason {
Eof,
Hash,
}
fn parse_param_list(b_iter: &mut ByteIterator) -> (Vec<ByteString>, ParamListTermReason) {
let mut params = vec![];
loop {
match b_iter.read() {
Some(b'#') => break,
None => return (params, ParamListTermReason::Eof),
_ => continue,
}
}
loop {
let (param, terminate) = parse_param(b_iter);
match terminate {
ParamTermReason::Hash => {
b_iter.rewind();
if !param.is_empty() {
params.push(param);
}
return (params, ParamListTermReason::Hash);
}
ParamTermReason::Eof => {
if !param.is_empty() {
params.push(param);
}
return (params, ParamListTermReason::Eof);
}
_ => {}
}
params.push(param);
}
}
enum ParamTermReason {
Hash,
Colon,
Semicolon,
Eof,
}
fn parse_param(b_iter: &mut ByteIterator) -> (ByteString, ParamTermReason) {
fn parse_val_inner(b_iter: &mut ByteIterator) -> ByteString {
let mut param = vec![];
loop {
let byte = read!(b_iter);
match byte {
b'\\' => {
let next_byte = read!(b_iter);
param.push(next_byte);
}
b'/' if b_iter.peek() == Some(&b'/') => {
let mut chomp = read!(b_iter);
while chomp != b'\n' {
chomp = read!(b_iter);
}
param.push(b'\n');
}
b'#' => {
for backtrack_byte in param.iter().rev() {
if *backtrack_byte == b'\n' {
return param.into();
}
if *backtrack_byte == b' ' || *backtrack_byte == b'\t' {
continue;
}
break;
}
param.push(byte);
}
b':' | b';' => {
return param.into();
}
_ => {
param.push(byte)
}
}
}
param.into()
}
let trimmed = parse_val_inner(b_iter).trim_ascii().into();
let t_reason = match b_iter.prev() {
Some(b'#') => ParamTermReason::Hash,
Some(b':') => ParamTermReason::Colon,
Some(b';') => ParamTermReason::Semicolon,
None => ParamTermReason::Eof,
u => panic!("Unexpected terminator {u:?}"),
};
(trimmed, t_reason)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::test_file_read;
use pretty_assertions::assert_eq;
macro_rules! test_parse_prm {
($input: expr, $index: expr, $out: expr) => {{
let mut biter = ByteIterator::new($input);
biter.set_index($index);
assert_eq!(
std::str::from_utf8(&parse_param(&mut biter).0).unwrap(),
$out
)
}};
}
#[test]
fn p_val() {
test_parse_prm!(b"#TITLE:AMONG US;", 1, "TITLE");
test_parse_prm!(b"#TITLE:AMONG US;", 7, "AMONG US");
}
#[test]
fn newlines() {
test_parse_prm!(
b"#NOTEDATA:
0000
0100
0000
0001, // measure 1
0000
0000
0000
0000
, // measure 2
;",
10,
"0000
0100
0000
0001,
0000
0000
0000
0000
,"
);
test_parse_prm!(
b"#TITLE: New Line
Here;",
7,
"New Line
Here"
);
test_parse_prm!(
b"#TITLE: Missing Semicolon
#ARTIST: foo;",
7,
"Missing Semicolon"
);
test_parse_prm!(
b"#TITLE: Missing Semicolon wspace
#ARTIST: foo;",
7,
"Missing Semicolon wspace"
);
test_parse_prm!(
b"#TITLE: Missing Semicolon
heyo #ARTIST: foo;",
7,
"Missing Semicolon
heyo #ARTIST"
);
}
#[test]
fn escape() {
test_parse_prm!(br"#TITLE: foo\:bar;", 7, r"foo:bar");
test_parse_prm!(br"#TITLE: foo\\:bar;", 7, r"foo\");
test_parse_prm!(br"#TITLE: foo\\\:bar;", 7, r"foo\:bar");
test_parse_prm!(
br"#TITLE: foo\
#artist",
7,
r"foo"
);
}
#[test]
fn multival() {
test_parse_prm!(b"#MULTI:VALUE:VALUE2;", 7, "VALUE");
test_parse_prm!(b"#MULTI:VALUE:VALUE2;", 13, "VALUE2");
}
#[test]
fn comment() {
test_parse_prm!(b"#TITLE: A/BCD;", 7, "A/BCD");
test_parse_prm!(b"#TITLE: A//BCD;", 7, "A");
}
macro_rules! test_parse_plist {
($input: expr, $out: expr) => {{
let mut biter = ByteIterator::new($input);
assert_eq!(
parse_param_list(&mut biter)
.0
.iter()
.map(|b| std::str::from_utf8(&b).unwrap())
.collect::<Vec<_>>(),
$out
);
}};
}
#[test]
fn list() {
test_parse_plist!(b"#TITLE:FOO", vec!["TITLE", "FOO"]);
test_parse_plist!(b"#TITLE:FOO;BAR:BAZ", vec!["TITLE", "FOO", "BAR", "BAZ"]);
test_parse_plist!(
b"#TITLE:FOO;
#ARTIST:AMONG_US;",
vec!["TITLE", "FOO"]
);
test_parse_plist!(b"#TITLE:FOO;#BAR:BAZ", vec!["TITLE", "FOO", "#BAR", "BAZ"]);
test_parse_plist!(
b"#TITLE:FOO
;#BAR:BAZ",
vec!["TITLE", "FOO", "#BAR", "BAZ"]
);
test_parse_plist!(
b"#TITLE:FOO
#BAR:BAZ",
vec!["TITLE", "FOO"]
);
test_parse_plist!(
b"#TITLE:FOO:
#BAR:BAZ",
vec!["TITLE", "FOO"]
);
test_parse_plist!(
br"#TITLE:FOO:
\ #BAR:BAZ",
vec!["TITLE", "FOO"]
);
}
fn full_comp(buf: &[u8], expected: Vec<Vec<&str>>) {
let parse_res = from_bytes(buf);
for (i, values) in expected.iter().enumerate() {
let e = parse_res.elements[i].clone();
assert_eq!(
std::str::from_utf8(&e.tag).unwrap(),
values.first().unwrap().to_owned()
);
assert_eq!(
e.values
.into_iter()
.map(|f| std::str::from_utf8(&f).unwrap().to_owned())
.collect::<Vec<String>>(),
values
.iter()
.skip(1)
.map(|f| f.to_owned().to_owned())
.collect::<Vec<String>>()
);
}
}
#[test]
fn full_parse() {
let expected = vec![
vec!["TITLE", "[11] [120] Le Perv (zk test ed.)"],
vec!["ARTIST", "Carpenter Brut"],
vec!["SUBTITLE", ""],
];
full_comp(&test_file_read("zk-test.sm"), expected);
}
#[test]
fn full_parse_simple() {
let expected = vec![
vec!["TITLE", "Song Title"],
vec!["ARTIST", "Song Artist"],
vec!["SUBTITLE", ""],
vec!["WHITESPACE", "Foo"],
];
full_comp(
br"#TITLE:Song Title;
#Artist:Song Artist;
#SUBTITLE:;
#WHITESPACE: Foo;",
expected,
);
}
}