use crate::cbin::{self, Cbin, Header, RawBody};
use crate::error::{Error, ParseError};
use std::fmt;
use std::fmt::Debug;
use std::io::{Read, Seek, Write};
pub const FORMAT: &str = "npno";
pub const CNSP_MAGIC: &[u8; 4] = b"CNSP";
const NAME_AT: usize = 0x1c;
const NAME_END: usize = 0x40;
const KEY_MAP_AT: usize = 0x8c;
pub const KNOWN_VERSIONS: &[u16] = &[0x450, 0x464];
const KNOWN_VERSIONS_U32: &[u32] = &[0x450, 0x464];
pub struct Piano {
pub file: Cbin<RawBody>,
}
impl Piano {
pub fn new() -> Piano {
Piano {
file: Cbin {
header: Header::new(FORMAT, (0, 0), 0),
body: RawBody(Vec::new()),
},
}
}
pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Piano, Error> {
Ok(Piano {
file: cbin::read(reader, FORMAT)?,
})
}
pub fn write_to(&self, writer: &mut (impl Write + Seek)) -> Result<(), Error> {
self.file.write_to(writer)
}
}
impl Default for Piano {
fn default() -> Self {
Self::new()
}
}
impl Piano {
fn cnsp(&self) -> Result<&[u8], Error> {
let body = &self.file.body.0;
if body.get(..4) != Some(CNSP_MAGIC.as_slice()) {
return Err(ParseError::AssertFail(format!(
"body opens {:02x?}, not the CNSP stream",
body.get(..4).unwrap_or_default()
))
.into());
}
Ok(body)
}
fn mapped(&self) -> Result<&[u8], Error> {
let version = self.stream_version()?;
crate::formats::known_version(FORMAT, u32::from(version), KNOWN_VERSIONS_U32)?;
self.cnsp()
}
pub fn stream_version(&self) -> Result<u16, Error> {
let body = self.cnsp()?;
let bytes = body.get(4..6).ok_or_else(|| {
ParseError::AssertFail("body ends inside the CNSP header".to_string())
})?;
Ok(u16::from_be_bytes(bytes.try_into().unwrap()))
}
pub fn name(&self) -> Result<(String, String), Error> {
let body = self.mapped()?;
let field = body
.get(NAME_AT..NAME_END)
.ok_or_else(|| ParseError::AssertFail("body ends inside the name field".to_string()))?;
let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
let text = String::from_utf8_lossy(&field[..end]);
let (name, variant) = text.split_once('#').unwrap_or((&text, ""));
Ok((name.trim().to_owned(), variant.trim().to_owned()))
}
pub fn key_map(&self) -> Result<&[u8], Error> {
self.mapped()?
.get(KEY_MAP_AT..KEY_MAP_AT + 128)
.ok_or_else(|| {
ParseError::AssertFail("body ends inside the key map".to_string()).into()
})
}
}
impl Debug for Piano {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("npno::Piano")
.field("header", &self.file.header)
.field("body_len", &self.file.body.0.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn piano(name_field: &[u8]) -> Piano {
let mut body = vec![0u8; KEY_MAP_AT + 128];
body[..4].copy_from_slice(CNSP_MAGIC);
body[4..6].copy_from_slice(&0x0450u16.to_be_bytes());
body[NAME_AT..NAME_AT + name_field.len()].copy_from_slice(name_field);
for (i, slot) in body[KEY_MAP_AT..].iter_mut().enumerate() {
*slot = if i < 12 { 0xFF } else { 0x19 };
}
Piano {
file: Cbin {
header: Header::new(FORMAT, (0, 0), 530),
body: RawBody(body),
},
}
}
#[test]
fn the_name_field_splits_on_the_separator() {
let p = piano(b"Electric Grand 1#CP80 ");
assert_eq!(p.stream_version().unwrap(), 0x450);
let (name, variant) = p.name().unwrap();
assert_eq!(name, "Electric Grand 1");
assert_eq!(variant, "CP80");
let p = piano(b"Clavinet D6# ");
assert_eq!(p.name().unwrap(), ("Clavinet D6".into(), "".into()));
}
#[test]
fn the_key_map_is_read_verbatim() {
let p = piano(b"X#");
let map = p.key_map().unwrap();
assert_eq!(map.len(), 128);
assert_eq!(map[0], 0xFF);
assert_eq!(map[127], 0x19);
}
#[test]
fn an_unknown_stream_version_still_round_trips_but_does_not_decode() {
let mut p = piano(b"X#");
p.file.body.0[4..6].copy_from_slice(&0x0500u16.to_be_bytes());
assert_eq!(p.stream_version().unwrap(), 0x500);
assert!(
p.name().is_err(),
"the name offset is only pinned on known versions"
);
assert!(p.key_map().is_err());
}
#[test]
fn a_body_without_the_magic_is_refused() {
let mut p = piano(b"X#");
p.file.body.0[0] = b'Q';
assert!(p.name().is_err(), "a non-CNSP body has no name to read");
}
}