use std::ops::Range;
use std::path::{Path, PathBuf};
use nord_format::cbin::{Generation, Header};
use nord_format::formats::{ne5, npno, nsmp};
use nord_format::{Entity, Live, Program};
use nord_usb::ObjectClass;
use crate::slot::shown;
use crate::ui::Ui;
pub(crate) fn tag(class: ObjectClass) -> Option<&'static str> {
match class {
ObjectClass::Piano => Some(npno::FORMAT),
ObjectClass::Sample => Some(nsmp::FORMAT),
ObjectClass::Program => Some(ne5::program::FORMAT),
ObjectClass::SetList => Some(ne5::song::FORMAT),
ObjectClass::Live => Some(ne5::live::FORMAT),
ObjectClass::Settings => Some(ne5::settings::FORMAT),
ObjectClass::Unknown(_) => None,
}
}
const NAMED: [ObjectClass; 6] = [
ObjectClass::Piano,
ObjectClass::Sample,
ObjectClass::Program,
ObjectClass::SetList,
ObjectClass::Live,
ObjectClass::Settings,
];
pub(crate) fn noun(format: &str) -> Option<String> {
NAMED
.into_iter()
.find(|&class| tag(class) == Some(format))
.map(crate::slot::noun)
}
fn check(path: &Path, format: &str, class: ObjectClass) -> Result<(), String> {
match tag(class) {
Some(want) if want != format => {
let steer = match noun(format) {
Some(n) => format!(" — try `nord {n}`"),
None => String::new(),
};
Err(format!(
"{}: a {format} file, but this command reads {} ({want}){steer}",
path.display(),
class.label(),
))
}
_ => Ok(()),
}
}
pub(crate) fn checksum_range(generation: Generation, len: usize) -> Option<Range<usize>> {
match generation {
Generation::V0 => len.checked_sub(2).map(|at| at..len),
Generation::V1 => (len >= 0x1c).then_some(0x18..0x1c),
}
}
fn crc(header: &Header, bytes: &[u8]) -> (&'static str, String) {
let stored = checksum_range(header.generation, bytes.len()).and_then(|at| bytes.get(at));
match (header.generation, stored) {
(Generation::V0, Some(b)) => (
"crc16:",
format!("{:#06x}", u16::from_le_bytes(b.try_into().unwrap())),
),
(Generation::V1, Some(b)) => (
"crc32:",
format!("{:#010x}", u32::from_le_bytes(b.try_into().unwrap())),
),
_ => ("crc:", "not in these bytes".to_string()),
}
}
pub fn get(
ui: &Ui,
path: &Path,
out: Option<PathBuf>,
class: ObjectClass,
body: bool,
) -> Result<(), String> {
let file = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
let read = nord_usb::envelope::unwrap(&file).map_err(|e| format!("{}: {e}", path.display()))?;
let format = nord_usb::envelope::tag(&read.header);
check(path, &format, class)?;
match (body, out) {
(true, Some(out)) => {
let wire_body = &read.body.0;
crate::edit::replace_file(&out, wire_body)?;
ui.note(format!(
"unwrapped the {format} body of {} -> {} ({} bytes)",
path.display(),
out.display(),
wire_body.len(),
));
Ok(())
}
(true, None) => Err("--body writes a file; give -o a path".into()),
(false, Some(_)) => Err(format!(
"{} is already a file; -o has nothing to save (--body extracts the wire body)",
path.display()
)),
(false, None) => {
let entity = nord_format::from_stream(&mut std::io::Cursor::new(&file))
.map_err(|e| format!("{}: {e}", path.display()))?;
crate::summary::print(ui, &entity);
Ok(())
}
}
}
pub fn info(ui: &Ui, path: &Path, class: ObjectClass) -> Result<(), String> {
let file = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
let read = nord_usb::envelope::unwrap(&file).map_err(|e| format!("{}: {e}", path.display()))?;
let format = nord_usb::envelope::tag(&read.header);
check(path, &format, class)?;
let at = nord_usb::envelope::location(&read.header);
let body_len = u32::try_from(read.body.0.len()).map_err(|_| {
format!(
"{}: body length does not fit the file format",
path.display()
)
})?;
let (crc_label, crc_value) = crc(&read.header, &file);
let row = |label: &str, value: String| {
ui.out(format!(" {}{value}", ui.dim(format!("{label:<11}"))));
};
if (at.bank, at.slot) == (0xffff, 0xffff) {
row(
"location:",
format!("none {}", ui.dim("(a library file, not a slot save)")),
);
} else {
row(
"location:",
format!(
"{} {}",
shown(at),
ui.dim("(the slot the file was saved from)")
),
);
}
row(
"name:",
format!(
"none {}",
ui.dim("(the CBIN header has no name; inspect reads the body)")
),
);
row("format:", format);
row("version:", read.header.version.to_string());
row(
"body:",
format!(
"{} bytes{}",
crate::device::grouped(body_len),
match crate::device::human_size(body_len) {
Some(h) => format!(" {}", ui.dim(format!("({h})"))),
None => String::new(),
}
),
);
row(crc_label, crc_value);
Ok(())
}
pub fn deps(ui: &Ui, path: &Path, class: ObjectClass) -> Result<(), String> {
let entity = nord_format::from_path(path).map_err(|e| format!("{}: {e}", path.display()))?;
let format = entity_tag(&entity);
check(path, format, class)?;
let (piano, sample) = match &entity {
Entity::Program(Program::Electro5(p)) => (p.piano_panel.id.id(), p.sample_panel.id.id()),
Entity::Live(Live::Electro5(l)) => (l.piano_panel.id.id(), l.sample_panel.id.id()),
Entity::Song(_) => {
return Err("a set list names program slots, not library objects; \
`nord setlist deps BANK:SLOT` asks the instrument, which resolves them"
.into())
}
_ => {
return Err(format!(
"{}: a {format} file carries no dependency ids",
path.display()
))
}
};
let refs: Vec<(ObjectClass, u32)> =
[(ObjectClass::Piano, piano), (ObjectClass::Sample, sample)]
.into_iter()
.filter(|&(_, id)| id != 0)
.collect();
if refs.is_empty() {
ui.note(format!("{} references no library objects", path.display()));
return Ok(());
}
ui.out(ui.dim(format!("{:<8} id", "class")));
for (class, id) in &refs {
ui.out(format!(
"{:<8} {}",
class.label(),
crate::summary::dep_id(*id)
));
}
ui.note(ui.dim("(ids only — the names live on the instrument; `deps BANK:SLOT` shows them)"));
Ok(())
}
pub(crate) fn entity_tag(entity: &Entity) -> &'static str {
entity.identity().format
}
pub(crate) fn first_difference(a: &[u8], b: &[u8]) -> String {
match a.iter().zip(b).position(|(x, y)| x != y) {
Some(at) => format!("{at:#x}"),
None => "the end (the lengths differ)".to_string(),
}
}
pub(crate) fn check_each<T>(
ui: &Ui,
targets: &[T],
what: &str,
mut check: impl FnMut(&T) -> Result<String, String>,
) -> Result<(), String> {
let mut failed = 0usize;
for target in targets {
match check(target) {
Ok(line) => ui.out(line),
Err(line) => {
failed += 1;
ui.out(line);
}
}
}
match failed {
0 => Ok(()),
n => Err(format!("{n} of {} {what}", targets.len())),
}
}
#[cfg(test)]
mod tests {
use super::*;
use nord_usb::wire::Location;
#[test]
fn the_header_fields_come_back_out_of_a_wrapped_file() {
let at = Location::from_user(7, 4);
let a = nord_usb::envelope::wrap("ne5p", at, 4, &[0u8; 8]).unwrap();
let b = nord_usb::envelope::wrap("ne5p", at, 4, &[1u8; 8]).unwrap();
let c = nord_usb::envelope::wrap("ne5t", at, 1, &[0u8; 8]).unwrap();
let header = |file: &[u8]| nord_usb::envelope::unwrap(file).unwrap().header;
assert_eq!(header(&a).version, 4);
assert_eq!(header(&c).version, 1);
assert_eq!(crc(&header(&a), &a).0, "crc32:");
assert_ne!(crc(&header(&a), &a).1, crc(&header(&b), &b).1);
}
#[test]
fn a_type_0_file_reports_its_trailing_crc16() {
let mut program = ne5::program::new((3, 7).try_into().unwrap());
program.header.generation = Generation::V0;
let mut bytes = Vec::new();
program
.write_to(&mut std::io::Cursor::new(&mut bytes))
.unwrap();
let header = nord_usb::envelope::unwrap(&bytes).unwrap().header;
assert_eq!(header.version, 4);
let (label, value) = crc(&header, &bytes);
assert_eq!(label, "crc16:");
let stored = u16::from_le_bytes(bytes[bytes.len() - 2..].try_into().unwrap());
assert_eq!(value, format!("{stored:#06x}"));
assert_eq!(u16::from_be_bytes([bytes[0x18], bytes[0x19]]), 4);
}
#[test]
fn a_round_trip_that_differs_says_where_it_first_did() {
assert_eq!(first_difference(b"abcd", b"abed"), "0x2");
assert_eq!(first_difference(b"Xbcd", b"abcd"), "0x0");
let ran_out = "the end (the lengths differ)";
assert_eq!(first_difference(b"abcd", b"abcde"), ran_out);
assert_eq!(first_difference(b"", b"a"), ran_out);
}
#[test]
fn a_check_of_many_targets_counts_what_failed() {
let ui = Ui::piped();
let verdict = |target: &&str| match *target {
"bad" => Err("DIFFER bad".to_string()),
ok => Ok(format!("ok {ok}")),
};
let what = "file(s) did not round-trip";
assert!(check_each(&ui, &["a", "b"], what, verdict).is_ok());
assert_eq!(
check_each(&ui, &["a", "bad", "bad"], what, verdict).unwrap_err(),
"2 of 3 file(s) did not round-trip"
);
}
#[test]
fn a_wrong_class_is_steered_to_the_right_noun() {
let err = check(Path::new("x.ne5t"), "ne5t", ObjectClass::Program).unwrap_err();
assert!(err.contains("nord setlist"), "{err}");
assert!(check(Path::new("x.ne5t"), "ne5t", ObjectClass::SetList).is_ok());
assert!(check(Path::new("x.bin"), "abcd", ObjectClass::Unknown(9)).is_ok());
}
}