use crate::error::{Error, Result};
pub const HEADER_LEN: usize = 16;
pub const CRC_LEN: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Service {
Ui,
Program,
Unknown(u32),
}
impl Service {
pub fn from_raw(v: u32) -> Self {
match v {
6 => Service::Ui,
12 => Service::Program,
other => Service::Unknown(other),
}
}
pub fn to_raw(self) -> u32 {
match self {
Service::Ui => 6,
Service::Program => 12,
Service::Unknown(v) => v,
}
}
}
pub mod cmd {
pub const SESSION_OPEN: u32 = 0x04;
pub const SESSION_CLOSE: u32 = 0x06;
pub const STATUS: u32 = 0x08;
pub const DELETE: u32 = 0x14;
pub const READ: u32 = 0x12;
pub const COPY: u32 = 0x16;
pub const MOVE: u32 = 0x18;
pub const INFO: u32 = 0x1e;
pub const RENAME: u32 = 0x1c;
pub const SELECT: u32 = 0x2f;
pub const RELINK: u32 = 0x35;
pub const BEGIN_WRITE: u32 = 0x0a;
pub const WRITE_PREPARE: u32 = 0x22;
pub const WRITE_PREPARE_2: u32 = 0x26;
pub const BEGIN_READ: u32 = 0x0c;
pub const END_TRANSFER: u32 = 0x0e;
pub const WRITE_DATA: u32 = 0x10;
pub const DEPENDENCIES: u32 = 0x28;
pub const PARTITIONS: u32 = 0x00;
pub const BANKS: u32 = 0x02;
pub const FOCUS: u32 = 0x31;
pub const NEXT_SLOT: u32 = 0x20;
pub const ERASE_ALL: u32 = 0x24;
pub const HIGHEST_ANSWERING: u32 = 0x3d;
pub const NOTIFY_READ_WEDGE: u32 = 0x2a;
pub const CHANGED: u32 = 0x2c;
pub const NOTIFY_ENABLE: u32 = 0x2d;
}
pub mod ui {
use super::{Message, Service};
use crate::error::{Error, Result};
pub const SUBSYSTEM: u32 = 1;
pub const HELLO: u32 = 0x00;
pub const GOODBYE: u32 = 0x02;
pub const LABEL: u32 = 0x06;
pub const PERCENT: u32 = 0x07;
pub const MAX_LABEL_LEN: usize = u8::MAX as usize;
pub fn label(text: &str) -> Result<Message> {
if text.len() > MAX_LABEL_LEN {
return Err(Error::InvalidArgument(format!(
"progress label is {} bytes; the length field holds at most {MAX_LABEL_LEN}",
text.len(),
)));
}
let mut args = vec![0u8; 6];
args.push(text.len() as u8);
args.extend_from_slice(text.as_bytes());
Ok(Message::new(Service::Ui, SUBSYSTEM, LABEL, args))
}
pub fn percent(pct: u16) -> Message {
let mut args = 1u16.to_be_bytes().to_vec();
args.extend_from_slice(&pct.min(100).to_be_bytes());
Message::new(Service::Ui, SUBSYSTEM, PERCENT, args)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramInfo {
pub location: Location,
pub body_len: u32,
pub format: String,
pub version: u32,
pub crc32: Option<u32>,
pub name: String,
}
impl ProgramInfo {
const NAME_LEN_AT: usize = 28;
pub fn decode(msg: &Message) -> Result<Self> {
if !msg.is_response() {
return Err(Error::InvalidArgument(
"object info must be decoded from a response (use Message::decode_response)".into(),
));
}
let p = msg.payload();
if p.len() < Self::NAME_LEN_AT + 4 {
return Err(Error::Truncated {
got: p.len(),
need: Self::NAME_LEN_AT + 4,
});
}
let word = |i: usize| u32::from_be_bytes(p[i..i + 4].try_into().unwrap());
let name_len = word(Self::NAME_LEN_AT) as usize;
let name_start = Self::NAME_LEN_AT + 4;
let name_end = checked_end(p, name_start, name_len)?;
let name = String::from_utf8_lossy(&p[name_start..name_end])
.trim_end()
.to_owned();
let crc32 = match p.len().saturating_sub(name_end) >= 4 {
true => match word(p.len() - 4) {
u32::MAX => None,
crc => Some(crc),
},
false => None,
};
Ok(Self {
location: Location {
bank: word(0),
slot: word(4),
},
body_len: word(8),
format: String::from_utf8_lossy(&p[12..16]).into_owned(),
version: word(16),
crc32,
name,
})
}
}
const PARTITION_FIELDS: usize = 29;
fn read_u32(buf: &[u8], at: usize) -> Result<u32> {
let end = at.checked_add(4).ok_or(Error::Truncated {
got: buf.len(),
need: usize::MAX,
})?;
buf.get(at..end)
.map(|b| u32::from_be_bytes(b.try_into().unwrap()))
.ok_or(Error::Truncated {
got: buf.len(),
need: end,
})
}
fn checked_end(buf: &[u8], start: usize, len: usize) -> Result<usize> {
let end = start.checked_add(len).ok_or(Error::Truncated {
got: buf.len(),
need: usize::MAX,
})?;
if end > buf.len() {
return Err(Error::Truncated {
got: buf.len(),
need: end,
});
}
Ok(end)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Partition {
pub index: u32,
pub name: String,
pub native: bool,
pub fields: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bank {
pub index: u32,
pub name: String,
pub slots: u32,
}
impl Partition {
pub fn decode_all(msg: &Message) -> Result<Vec<Self>> {
if !msg.is_response() {
return Err(Error::InvalidArgument(
"partitions must be decoded from a response".into(),
));
}
let p = msg.payload();
let count = *p.first().ok_or(Error::Truncated { got: 0, need: 1 })? as usize;
let mut out = Vec::with_capacity(count);
let mut at = 1;
for index in 0..count {
let len = read_u32(p, at)? as usize;
let name_start = checked_end(p, at, 4)?;
let end = checked_end(p, name_start, len)?;
let fields_end = checked_end(p, end, PARTITION_FIELDS)?;
let name = String::from_utf8_lossy(&p[name_start..end])
.trim_end()
.to_string();
out.push(Partition {
index: index as u32,
native: name.contains("(Native)"),
name,
fields: p[end..fields_end].to_vec(),
});
at = fields_end;
}
Ok(out)
}
}
impl Bank {
pub fn decode_all(msg: &Message) -> Result<Vec<Self>> {
if !msg.is_response() {
return Err(Error::InvalidArgument(
"banks must be decoded from a response".into(),
));
}
let p = msg.payload();
let count = *p.get(4).ok_or(Error::Truncated {
got: p.len(),
need: 5,
})? as usize;
let mut out = Vec::with_capacity(count);
let mut at = 5;
for index in 0..count {
let len = read_u32(p, at)? as usize;
let name_start = checked_end(p, at, 4)?;
let end = checked_end(p, name_start, len)?;
let name = String::from_utf8_lossy(&p[name_start..end])
.trim_end()
.to_string();
out.push(Bank {
index: index as u32,
name,
slots: read_u32(p, end)?,
});
at = end + 4;
}
Ok(out)
}
pub const UNBOUNDED: u32 = 0xfffe;
pub fn is_bounded(&self) -> bool {
self.slots != Self::UNBOUNDED
}
}
pub struct Dependency {
pub flag: u8,
pub class: ObjectClass,
pub id: u32,
pub name: String,
pub location: Option<Location>,
}
impl Dependency {
pub fn is_required(&self) -> bool {
self.flag == 1 && (self.id != 0 || self.location.is_some())
}
pub fn decode_all(msg: &Message) -> Result<Vec<Self>> {
if !msg.is_response() {
return Err(Error::InvalidArgument(
"dependency list must be decoded from a response (use Message::decode_response)"
.into(),
));
}
let p = msg.payload();
if p.len() < 12 {
return Err(Error::Truncated {
got: p.len(),
need: 12,
});
}
let word = |i: usize| u32::from_be_bytes(p[i..i + 4].try_into().unwrap());
let count = word(8) as usize;
let mut out = Vec::with_capacity(count.min((p.len() - 12) / 29));
let mut i = 12;
for _ in 0..count {
let name_start = checked_end(p, i, 17)?;
let flag = p[i];
let class = ObjectClass::from_raw(word(i + 5));
let id = word(i + 9);
let name_len = word(i + 13) as usize;
let name_end = checked_end(p, name_start, name_len)?;
let record_end = checked_end(p, name_end, 12)?;
let name = String::from_utf8_lossy(&p[name_start..name_end]).into_owned();
let has_location = word(name_end) != 0;
let location = has_location.then(|| Location {
bank: word(name_end + 4),
slot: word(name_end + 8),
});
out.push(Self {
flag,
class,
id,
name,
location,
});
i = record_end;
}
Ok(out)
}
}
pub fn crc16(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
crc = if crc & 0x8000 != 0 {
(crc << 1) ^ 0x1021
} else {
crc << 1
};
}
}
crc
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub service: Service,
pub subsystem: u32,
pub command: u32,
pub args: Vec<u8>,
is_response: bool,
}
#[cfg(feature = "fault-injection")]
fn protocol_version_override() -> Option<u32> {
std::env::var("NORD_PROTOCOL_VERSION")
.ok()
.and_then(|v| v.parse().ok())
}
#[cfg(not(feature = "fault-injection"))]
fn protocol_version_override() -> Option<u32> {
None
}
impl Message {
pub fn new(service: Service, subsystem: u32, command: u32, args: Vec<u8>) -> Self {
let subsystem = match service {
Service::Program => protocol_version_override().unwrap_or(subsystem),
_ => subsystem,
};
Self {
service,
subsystem,
command,
args,
is_response: false,
}
}
pub fn is_response(&self) -> bool {
self.is_response
}
pub fn status(&self) -> Option<u32> {
if !self.is_response || self.command == cmd::CHANGED || self.args.len() < 4 {
return None;
}
Some(u32::from_be_bytes(self.args[..4].try_into().ok()?))
}
pub fn payload(&self) -> &[u8] {
if self.is_response && self.command != cmd::CHANGED && self.args.len() >= 4 {
&self.args[4..]
} else {
&self.args
}
}
pub fn encode(&self) -> Vec<u8> {
let len = (HEADER_LEN + self.args.len() + CRC_LEN) as u32;
let mut out = Vec::with_capacity(len as usize);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(&self.service.to_raw().to_be_bytes());
out.extend_from_slice(&self.subsystem.to_be_bytes());
out.extend_from_slice(&self.command.to_be_bytes());
out.extend_from_slice(&self.args);
out.extend_from_slice(&crc16(&out).to_be_bytes());
out
}
pub fn decode_response(buf: &[u8]) -> Result<Self> {
let mut m = Self::decode(buf)?;
if m.command != cmd::CHANGED && buf.len() < HEADER_LEN + 4 + CRC_LEN {
return Err(Error::Truncated {
got: buf.len(),
need: HEADER_LEN + 4 + CRC_LEN,
});
}
m.is_response = true;
Ok(m)
}
pub fn decode_probe(buf: &[u8]) -> Result<Self> {
let mut m = Self::decode(buf)?;
m.is_response = true;
Ok(m)
}
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < HEADER_LEN + CRC_LEN {
return Err(Error::Truncated {
got: buf.len(),
need: HEADER_LEN + CRC_LEN,
});
}
let declared = u32::from_be_bytes(buf[0..4].try_into().unwrap()) as usize;
if declared != buf.len() {
return Err(Error::LengthMismatch {
declared,
actual: buf.len(),
});
}
let split = buf.len() - CRC_LEN;
let expected = u16::from_be_bytes(buf[split..].try_into().unwrap());
let actual = crc16(&buf[..split]);
if expected != actual {
return Err(Error::BadCrc { expected, actual });
}
Ok(Self {
service: Service::from_raw(u32::from_be_bytes(buf[4..8].try_into().unwrap())),
subsystem: u32::from_be_bytes(buf[8..12].try_into().unwrap()),
command: u32::from_be_bytes(buf[12..16].try_into().unwrap()),
args: buf[HEADER_LEN..split].to_vec(),
is_response: false,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectClass {
Piano,
Sample,
Program,
SetList,
Live,
Settings,
Unknown(u32),
}
impl ObjectClass {
pub fn from_raw(v: u32) -> Self {
match v {
1 => ObjectClass::Piano,
3 => ObjectClass::Sample,
4 => ObjectClass::Program,
5 => ObjectClass::SetList,
6 => ObjectClass::Live,
7 => ObjectClass::Settings,
other => ObjectClass::Unknown(other),
}
}
pub fn to_raw(self) -> u32 {
match self {
ObjectClass::Piano => 1,
ObjectClass::Sample => 3,
ObjectClass::Program => 4,
ObjectClass::SetList => 5,
ObjectClass::Live => 6,
ObjectClass::Settings => 7,
ObjectClass::Unknown(v) => v,
}
}
pub const INVENTORY: [ObjectClass; 4] = [
ObjectClass::Piano,
ObjectClass::Sample,
ObjectClass::Program,
ObjectClass::SetList,
];
pub fn label(self) -> String {
match self {
ObjectClass::Piano => "pianos".into(),
ObjectClass::Sample => "samples".into(),
ObjectClass::Program => "programs".into(),
ObjectClass::SetList => "set lists".into(),
ObjectClass::Live => "live slots".into(),
ObjectClass::Settings => "settings".into(),
ObjectClass::Unknown(v) => format!("class {v}"),
}
}
pub fn overwrites_in_place(self) -> bool {
matches!(self, ObjectClass::Live | ObjectClass::Settings)
}
pub fn names_its_slots(self) -> bool {
!matches!(self, ObjectClass::Live | ObjectClass::Settings)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Status {
pub class: ObjectClass,
pub count: u32,
pub free: u32,
pub used: u32,
}
impl Status {
pub fn total(&self) -> u64 {
u64::from(self.free) + u64::from(self.used)
}
pub fn blocks_per_item(&self) -> Option<u32> {
if self.count == 0 || self.used == 0 || !self.used.is_multiple_of(self.count) {
return None;
}
let per = self.used / self.count;
(per != 0 && self.total().is_multiple_of(u64::from(per))).then_some(per)
}
pub fn slots(&self) -> Option<u32> {
self.blocks_per_item()
.and_then(|per| u32::try_from(self.total() / u64::from(per)).ok())
}
pub fn used_percent(&self) -> f32 {
let total = self.total();
if total == 0 {
0.0
} else {
100.0 * self.used as f32 / total as f32
}
}
pub fn decode(class: ObjectClass, msg: &Message) -> Result<Self> {
if !msg.is_response() {
return Err(Error::InvalidArgument(
"status must be decoded from a response (use Message::decode_response)".into(),
));
}
let p = msg.payload();
if p.len() < 12 {
return Err(Error::Truncated {
got: p.len(),
need: 12,
});
}
let word = |i: usize| u32::from_be_bytes(p[i * 4..i * 4 + 4].try_into().unwrap());
Ok(Self {
class,
count: word(0),
free: word(1),
used: word(2),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Location {
pub bank: u32,
pub slot: u32,
}
impl Location {
pub fn from_user(bank: u32, slot: u32) -> Self {
assert!(
bank >= 1 && slot >= 1,
"from_user takes the panel's one-indexed numbering; got {bank}:{slot}"
);
Self {
bank: bank - 1,
slot: slot - 1,
}
}
pub fn user_bank(self) -> u64 {
u64::from(self.bank) + 1
}
pub fn user_slot(self) -> u64 {
u64::from(self.slot) + 1
}
pub fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.bank.to_be_bytes());
out.extend_from_slice(&self.slot.to_be_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
const MOVE: &str = "000000220000000c0000000a00000018000000070000000c000000060000000f4a55";
const MOVE_RESP: &str =
"000000260000000c0000000a0000001900000000000000070000000c000000060000000f7197";
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn only_the_buffer_classes_overwrite_in_place_and_hold_no_name() {
for class in [ObjectClass::Live, ObjectClass::Settings] {
assert!(class.overwrites_in_place(), "{}", class.label());
assert!(!class.names_its_slots(), "{}", class.label());
}
let storage = [
ObjectClass::Piano,
ObjectClass::Sample,
ObjectClass::Program,
ObjectClass::SetList,
ObjectClass::Unknown(9),
];
for class in storage {
assert!(!class.overwrites_in_place(), "{}", class.label());
assert!(class.names_its_slots(), "{}", class.label());
}
}
#[test]
fn decodes_a_real_move() {
let m = Message::decode(&hex(MOVE)).unwrap();
assert_eq!(m.service, Service::Program);
assert_eq!(m.subsystem, 10);
assert_eq!(m.command, cmd::MOVE);
assert!(!m.is_response());
let mut want = Vec::new();
Location::from_user(8, 13).write_to(&mut want);
Location::from_user(7, 16).write_to(&mut want);
assert_eq!(m.payload(), want.as_slice());
}
#[test]
fn response_is_request_plus_one_plus_status() {
let req = Message::decode(&hex(MOVE)).unwrap();
let resp = Message::decode_response(&hex(MOVE_RESP)).unwrap();
assert_eq!(resp.command, req.command + 1);
assert!(resp.is_response());
assert_eq!(resp.status(), Some(0));
assert_eq!(resp.payload(), req.payload());
assert_eq!(hex(MOVE_RESP).len() - hex(MOVE).len(), 4);
}
#[test]
fn direction_is_not_inferable_from_command_parity() {
let req =
Message::decode(&hex("0000001a0000000c0000000a0000002f00000000000000017f71")).unwrap();
assert_eq!(req.command, 0x2f);
assert!(req.command & 1 == 1, "this request really is odd-numbered");
assert!(
!req.is_response(),
"an odd command must still decode as a request"
);
assert_eq!(req.status(), None);
assert_eq!(req.payload().len(), 8);
let resp = Message::decode_response(&hex(
"0000001e0000000c0000000a0000003000000000000000000000000112c4",
))
.unwrap();
assert_eq!(resp.command, req.command + 1);
assert!(
resp.command & 1 == 0,
"this response really is even-numbered"
);
assert!(resp.is_response());
assert_eq!(
resp.status(),
Some(0),
"status must be readable despite even command"
);
assert_eq!(
resp.payload(),
req.payload(),
"args line up once status is stripped"
);
}
#[test]
fn round_trips_byte_exact() {
for raw in [MOVE, MOVE_RESP] {
let bytes = hex(raw);
assert_eq!(Message::decode(&bytes).unwrap().encode(), bytes);
}
}
#[test]
fn rejects_a_corrupted_crc() {
let mut bytes = hex(MOVE);
*bytes.last_mut().unwrap() ^= 0xFF;
assert!(matches!(Message::decode(&bytes), Err(Error::BadCrc { .. })));
}
#[test]
fn a_response_without_a_status_word_is_truncated() {
let bytes = Message::new(Service::Program, 10, cmd::STATUS + 1, Vec::new()).encode();
assert!(matches!(
Message::decode_response(&bytes),
Err(Error::Truncated { need: 22, .. })
));
}
#[test]
fn changed_is_a_statusless_notification() {
let bytes = Message::new(Service::Program, 10, cmd::CHANGED, vec![1, 2, 3, 4]).encode();
let message = Message::decode_response(&bytes).unwrap();
assert_eq!(message.status(), None);
assert_eq!(message.payload(), [1, 2, 3, 4]);
}
#[test]
fn crc_matches_known_messages() {
for raw in [
"0000001200000006000000010000000006a1",
"000000160000000c0000000a0000000400000004a218",
"000000120000000c0000000a000000066500",
] {
assert!(Message::decode(&hex(raw)).is_ok(), "{raw}");
}
}
#[test]
fn ui_label_and_percent_match_the_wire() {
assert_eq!(
super::ui::label("Deleting...").unwrap().encode(),
hex("000000240000000600000001000000060000000000000b44656c6574696e672e2e2e7394"),
);
assert_eq!(
super::ui::percent(100).encode(),
hex("0000001600000006000000010000000700010064927b"),
);
}
#[test]
fn object_info_decodes_every_format() {
let cases: &[(&str, &str, u32, Option<u32>, &str)] = &[
("000000450000000c0000000a0000001f00000000000000050000000c000000796e65357000000004ffffffffffffffff00000003666f6f000000000000000021ab3d01a1ee",
"ne5p", 4, Some(0x21ab_3d01), "foo"),
("000000460000000c0000000a0000001f000000000000000000000007000000126e65357400000001ffffffffffffffff00000004746573740000000000000000dce9a145bf84",
"ne5t", 1, Some(0xdce9_a145), "test"),
("0000005c0000000c0000000a0000001f0000000000000000000000000c7db5446e706e6f0000021c5e98c95affffffff0000001a526f79616c204772616e64203344205961533620584c20352e340000000500000000ffffffffc30b",
"npno", 540, None, "Royal Grand 3D YaS6 XL 5.4"),
("000000610000000c0000000a0000001f0000000000000000000000000011da986e736d70000000c8554100ec000800000000001f41636f7573746963205069616e6f20335f5f4b6f7267206d6f6e6f20322e300000000000000000ffffffff366f",
"nsmp", 200, None, "Acoustic Piano 3__Korg mono 2.0"),
];
for (raw, format, version, crc32, name) in cases {
let info = ProgramInfo::decode(&Message::decode_response(&hex(raw)).unwrap()).unwrap();
assert_eq!(&info.format, format);
assert_eq!(info.version, *version, "{format}");
assert_eq!(info.crc32, *crc32, "{format}");
assert_eq!(&info.name, name);
}
}
#[test]
fn object_info_reads_names_longer_than_the_old_scan_bound() {
let info = ProgramInfo::decode(
&Message::decode_response(&hex(
"000000780000000c0000000a0000001f00000000000000000000004b002700f66e736d70000000c8554777330009000200000036332056696f6c696e7320534d5f4368616d6265726c696e5f4d4d6173746572206d6f6e6f20736d616c6c2076657273696f6e20322e300000000000000000ffffffff062d",
))
.unwrap(),
)
.unwrap();
assert_eq!(
info.name,
"3 Violins SM_Chamberlin_MMaster mono small version 2.0"
);
assert_eq!(info.name.len(), 54);
}
#[test]
fn over_long_labels_are_refused_not_truncated() {
assert!(super::ui::label(&"x".repeat(super::ui::MAX_LABEL_LEN)).is_ok());
assert!(super::ui::label(&"x".repeat(super::ui::MAX_LABEL_LEN + 1)).is_err());
}
#[test]
fn percent_clamps_to_100() {
assert_eq!(
super::ui::percent(101).encode(),
super::ui::percent(100).encode()
);
assert_eq!(
super::ui::percent(u16::MAX).encode(),
super::ui::percent(100).encode()
);
}
#[test]
fn dependencies_require_a_response() {
let raw = hex(
"000000820000000c0000000a0000002900000000000000060000000200000002000000000000000001d303b5f20000001a526f79616c204772616e64203344205961533620584c20352e3400000000ffffffffffffffff010000000000000003f2f5cadc0000000c6166726963615f73706c697400000000ffffffffffffffffc791",
);
assert!(Dependency::decode_all(&Message::decode(&raw).unwrap()).is_err());
assert!(Dependency::decode_all(&Message::decode_response(&raw).unwrap()).is_ok());
}
#[test]
fn decodes_real_dependencies() {
let resp = Message::decode_response(&hex(
"000000820000000c0000000a0000002900000000000000060000000200000002000000000000000001d303b5f20000001a526f79616c204772616e64203344205961533620584c20352e3400000000ffffffffffffffff010000000000000003f2f5cadc0000000c6166726963615f73706c697400000000ffffffffffffffffc791",
))
.unwrap();
let deps = Dependency::decode_all(&resp).unwrap();
assert_eq!(deps.len(), 2);
assert_eq!(deps[0].class, ObjectClass::Piano);
assert_eq!(deps[0].id, 0xd303_b5f2);
assert_eq!(deps[0].name, "Royal Grand 3D YaS6 XL 5.4");
assert_eq!(deps[0].location, None);
assert_eq!(deps[1].class, ObjectClass::Sample);
assert_eq!(deps[1].id, 0xf2f5_cadc);
assert_eq!(deps[1].name, "africa_split");
assert_eq!(deps[1].location, None);
assert!(!deps[0].is_required());
assert!(deps[1].is_required());
}
#[test]
fn a_live_row_addressing_nothing_is_not_required() {
let d = Dependency {
flag: 1,
class: ObjectClass::Piano,
id: 0,
name: String::new(),
location: None,
};
assert!(!d.is_required());
}
#[test]
fn set_list_dependencies_are_required_by_location_not_id() {
let mut args = Vec::new();
for w in [0u32, 0, 42, 4] {
args.extend_from_slice(&w.to_be_bytes());
}
for slot in [6u32, 2, 38, 40] {
args.push(1); for w in [0u32, 4, 0, 0, 1, 0, slot] {
args.extend_from_slice(&w.to_be_bytes());
}
}
assert_eq!(args.len() - 4, 128);
let raw = Message::new(Service::Program, 10, cmd::DEPENDENCIES + 1, args).encode();
let deps = Dependency::decode_all(&Message::decode_response(&raw).unwrap()).unwrap();
assert_eq!(deps.len(), 4);
let slots: Vec<u32> = deps.iter().map(|d| d.location.unwrap().slot).collect();
assert_eq!(slots, [6, 2, 38, 40]);
for d in &deps {
assert_eq!(d.class, ObjectClass::Program);
assert_eq!(d.id, 0);
assert!(d.name.is_empty());
assert!(d.is_required(), "a slot-addressed dependency is required");
}
}
}