diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..8462285 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,25 @@ +name: Rust + +on: + push: + branches: [ "main", "develop" ] + pull_request: + branches: [ "main", "develop" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose --all-features + - name: Run tests + run: cargo test --verbose --all-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 277b137..ffd19cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.2.4 +ci: add standard cargo github actions +docs: fix various typos +docs: online docs generated with all features enabled +fix: sysex7 / sysex8 payload iterator integration with jr headers +fix: sysex7 / sysex8 payload iterator panics when empty +test: add fuzzing target for sysex7 and sysex8 roundtrip + # 0.2.3 fix: handling messages example code fix: default features include cv2 not cv1 diff --git a/Cargo.toml b/Cargo.toml index 8424524..13afec7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "midi2" -version = "0.2.3" +version = "0.2.4" description = "Ergonomic, versatile, strong types wrapping MIDI 2.0 message data." edition = "2021" readme = "README.md" @@ -13,6 +13,7 @@ repository = "https://github.com/BenLeadbetter/midi2.git" [workspace] members = [ "midi2_proc", + "fuzz", ] [lib] @@ -33,8 +34,11 @@ ump-stream = [] [dependencies] derive_more = { version = "0.99.17", features = ["from"], default-features = false } -midi2_proc = { version = "0.2.3", path = "midi2_proc" } +midi2_proc = { version = "0.2.4", path = "midi2_proc" } ux = "0.1.6" [dev-dependencies] pretty_assertions = "1.4.0" + +[package.metadata.docs.rs] +all-features = true diff --git a/README.md b/README.md index 4403df0..5c36607 100644 --- a/README.md +++ b/README.md @@ -176,8 +176,7 @@ You'll want to setup midi2 without default features to compile without the `std` feature. ```toml -// Cargo.toml -midi2 = { version = "0.2.3", default-features = false, features = [], } +midi2 = { version = "0.2.4", default-features = false, features = ["channel-voice2", "sysex7"], } ``` ### Generic Representation @@ -254,7 +253,7 @@ owned.set_jitter_reduction(Some(JitterReduction::Timestamp(0x1234))); assert_eq!(owned.data(), &[0x0020_1234, 0x1AF3_4F00]) ``` -## Supports For Classical MIDI Byte Stream Messages +## Support For Classical MIDI Byte Stream Messages Messages which can be represented in classical midi byte stream format are also supported. To do this simply use a backing buffer over `u8` instead of `u32`! ✨🎩 diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..acf7534 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "midi2-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.midi2] +path = ".." +default-features = false +features = [ + "std", + "sysex8", + "sysex7", +] + +[[bin]] +name = "sysex8_payload_roundtrip" +path = "./fuzz_targets/sysex8_payload_roundtrip.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "sysex7_payload_roundtrip" +path = "./fuzz_targets/sysex7_payload_roundtrip.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/sysex7_payload_roundtrip.rs b/fuzz/fuzz_targets/sysex7_payload_roundtrip.rs new file mode 100644 index 0000000..96312d0 --- /dev/null +++ b/fuzz/fuzz_targets/sysex7_payload_roundtrip.rs @@ -0,0 +1,22 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use midi2::{prelude::*, sysex7::*}; + +fuzz_target!(|data: &[u8]| { + let to_u7 = |b: u8| u7::new(b & 0x7F); + let mut message = Sysex7::>::new(); + message.set_payload(data.iter().cloned().map(to_u7)); + + // payload is unchanged + let payload = message.payload().collect::>(); + assert_eq!( + payload, + data.iter().cloned().map(to_u7).collect::>() + ); + + // message is in a valid state + let mut buffer = Vec::new(); + buffer.extend_from_slice(message.data()); + let _ = Sysex7::try_from(&buffer[..]).expect("Valid data"); +}); diff --git a/fuzz/fuzz_targets/sysex8_payload_roundtrip.rs b/fuzz/fuzz_targets/sysex8_payload_roundtrip.rs new file mode 100644 index 0000000..ff6b219 --- /dev/null +++ b/fuzz/fuzz_targets/sysex8_payload_roundtrip.rs @@ -0,0 +1,18 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use midi2::{prelude::*, sysex8::*}; + +fuzz_target!(|data: &[u8]| { + let mut message = Sysex8::>::new(); + message.set_payload(data.iter().cloned()); + + // payload is unchanged + let payload = message.payload().collect::>(); + assert_eq!(payload, data.iter().cloned().collect::>()); + + // message is in a valid state + let mut buffer = Vec::new(); + buffer.extend_from_slice(message.data()); + let _ = Sysex8::try_from(&buffer[..]).expect("Valid data"); +}); diff --git a/midi2_proc/Cargo.toml b/midi2_proc/Cargo.toml index edc3716..dd84f8c 100644 --- a/midi2_proc/Cargo.toml +++ b/midi2_proc/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "midi2_proc" description = "Internal procedural macro crate. Only intended for use with midi2" -version = "0.2.3" +version = "0.2.4" edition = "2021" readme = "README.md" license = "MIT OR Apache-2.0" diff --git a/src/buffer.rs b/src/buffer.rs index e047278..2287d4a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,5 +1,5 @@ //! Generic backing buffers for messages wrapper types. -//! +//! //! All messages in midi2 are backed by a generic buffer type. //! //! A buffer can be any data type which returns a slice of `u32` or `u8`. @@ -10,8 +10,8 @@ //! * `[U; SIZE] where U: Unit` //! * `Vec where U: Unit` (with the `std` feature enabled) //! -//! The api of the message wrapper changes depending on the traits of the -//! backing buffer. +//! The api of the message wrapper changes depending on the traits of the +//! backing buffer. //! //! For example `&[U]` implements [Buffer] //! but doesn't implement [BufferMut] so messages @@ -29,7 +29,7 @@ //! assert_eq!(message.note(), u7::default()); //! //! // error[E0277]: the trait bound `&[u32]: BufferMut` is not satisfied -//! message.set_note(u7::new(0x60)); +//! message.set_note(u7::new(0x60)); //! ``` //! //! `[U: SIZE]` buffers implement [BufferMut], but only @@ -44,7 +44,7 @@ //! assert_eq!(message.try_set_payload(0..20), Ok(())); //! ``` //! `Vec` implements [BufferMut] and [BufferResize]. -//! Messages backed with with such buffers have the most powerfull api. +//! Messages backed with with such buffers have the most powerful api. //! //! ```rust //! use midi2::prelude::*; @@ -59,7 +59,7 @@ //! possible to create a custom backing buffer. //! //! One potential fancy use case might be to create a non-allocating -//! resizable buffer which uses an area allocator. +//! resizable buffer which uses an arena allocator. use crate::error::BufferOverflow; diff --git a/src/channel_voice2/note_on.rs b/src/channel_voice2/note_on.rs index 25043c3..55fe623 100644 --- a/src/channel_voice2/note_on.rs +++ b/src/channel_voice2/note_on.rs @@ -117,4 +117,12 @@ mod tests { }), ); } + + #[test] + fn empty_jr_data() { + assert_eq!( + NoteOn([0x0, 0x4898_5E03, 0x6A14_E98A, 0x0, 0x0]).data(), + &[0x4898_5E03, 0x6A14_E98A], + ); + } } diff --git a/src/message.rs b/src/message.rs index 1ae17ac..88759c2 100644 --- a/src/message.rs +++ b/src/message.rs @@ -87,6 +87,11 @@ impl<'a> core::convert::TryFrom<&'a [u32]> for UmpMessage<&'a [u32]> { Eq, )] #[non_exhaustive] +#[cfg(any( + feature = "channel-voice1", + feature = "sysex7", + feature = "system-common" +))] pub enum BytesMessage { #[cfg(feature = "channel-voice1")] ChannelVoice1(crate::channel_voice1::ChannelVoice1), @@ -96,6 +101,11 @@ pub enum BytesMessage { SystemCommon(crate::system_common::SystemCommon), } +#[cfg(any( + feature = "channel-voice1", + feature = "sysex7", + feature = "system-common" +))] impl<'a> core::convert::TryFrom<&'a [u8]> for BytesMessage<&'a [u8]> { type Error = crate::error::Error; fn try_from(buffer: &'a [u8]) -> Result { diff --git a/src/sysex7/README.md b/src/sysex7/README.md index 9f390d6..e04fa37 100644 --- a/src/sysex7/README.md +++ b/src/sysex7/README.md @@ -81,7 +81,7 @@ assert_eq!( ], ); -// Borrowed messages are immutable and their liftimes are +// Borrowed messages are immutable and their lifetimes are // tied to the original buffer. // // To create an owned version use the `Rebuffer` traits. diff --git a/src/sysex7/mod.rs b/src/sysex7/mod.rs index c7fc876..282d948 100644 --- a/src/sysex7/mod.rs +++ b/src/sysex7/mod.rs @@ -446,18 +446,23 @@ impl Sysex for Sysex7 { { match ::UNIT_ID { crate::buffer::UNIT_ID_U8 => PayloadIterator { - data: &self.0.buffer()[1..self.size() - 1], + data: &self.0.buffer()[1..self.data().len() - 1], payload_index: 0, packet_index: 0, size_cache: 0, }, crate::buffer::UNIT_ID_U32 => { - let data = &self.0.buffer()[..self.size()]; - let size_cache = - ::specialise_buffer_u32(data) - .chunks_exact(2) - .map(|packet| PayloadIterator::::packet_size(packet)) - .sum::(); + use crate::buffer::UmpPrivate; + + let jr_offset = self.0.buffer().specialise_u32().jitter_reduction().len(); + let data = &self.0.buffer()[jr_offset..]; + let size_cache = self + .data() + .specialise_u32() + .message() + .chunks_exact(2) + .map(PayloadIterator::::packet_size) + .sum::(); PayloadIterator { data, payload_index: 0, @@ -804,12 +809,14 @@ impl<'a, U: crate::buffer::Unit> PayloadIterator<'a, U> { } fn finished_ump(&self) -> bool { - self.data.len() / 2 == self.packet_index + self.size_cache == 0 } fn advance_ump(&mut self) { self.payload_index += 1; - self.size_cache -= 1; + if !self.finished_ump() { + self.size_cache -= 1; + } let current_packet_size = Self::packet_size(&self.data_ump()[self.packet_index * 2..self.packet_index * 2 + 2]); @@ -1454,6 +1461,66 @@ mod tests { ); } + #[test] + fn payload_ump_with_jr_header() { + assert_eq!( + Sysex7::try_from( + &[ + 0x0, + 0x3016_0001_u32, + 0x0203_0405, + 0x3026_0607, + 0x0809_0A0B, + 0x3026_0C0D, + 0x0E0F_1011, + 0x3026_1213, + 0x1415_1617, + 0x3036_1819, + 0x1A1B_1C1D, + ][..] + ) + .unwrap() + .payload() + .map(u8::from) + .collect::>(), + std::vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, + ], + ); + } + + #[test] + fn payload_ump_with_timestamp_jr_header() { + assert_eq!( + Sysex7::try_from( + &[ + 0x0020_0000_u32, + 0x3016_0001, + 0x0203_0405, + 0x3026_0607, + 0x0809_0A0B, + 0x3026_0C0D, + 0x0E0F_1011, + 0x3026_1213, + 0x1415_1617, + 0x3036_1819, + 0x1A1B_1C1D, + ][..] + ) + .unwrap() + .payload() + .map(u8::from) + .collect::>(), + std::vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, + ], + ); + } + #[test] fn payload_ump_nth() { let buffer = [ @@ -1579,12 +1646,28 @@ mod tests { #[test] fn set_payload_to_fixed_size_buffer_accidentally_missed_jr_header() { let mut message = Sysex7::<[u32; 8]>::try_new().unwrap(); - assert_eq!(message.try_set_payload((0..24).map(u7::new)), Err(crate::error::BufferOverflow)); + assert_eq!( + message.try_set_payload((0..24).map(u7::new)), + Err(crate::error::BufferOverflow) + ); } #[test] fn set_payload_to_fixed_size_buffer_with_overflow() { let mut message = Sysex7::<[u32; 9]>::try_new().unwrap(); - assert_eq!(message.try_set_payload((0..30).map(u7::new)), Err(crate::error::BufferOverflow)); + assert_eq!( + message.try_set_payload((0..30).map(u7::new)), + Err(crate::error::BufferOverflow) + ); + } + + #[test] + fn empty_payload_ump() { + assert_eq!( + Sysex7::>::new() + .payload() + .collect::>(), + std::vec![] + ); } } diff --git a/src/sysex8/mod.rs b/src/sysex8/mod.rs index f26526d..b22eb53 100644 --- a/src/sysex8/mod.rs +++ b/src/sysex8/mod.rs @@ -20,7 +20,6 @@ struct Sysex8 { #[property(ConsistentStatuses)] #[readonly] consistent_statuses: (), - #[readonly] #[property(ValidPacketSizes)] valid_packet_sizes: (), #[property(GroupProperty)] @@ -67,9 +66,7 @@ impl crate::detail::property::Property for ValidPacket } impl<'a, B: crate::buffer::Ump> crate::detail::property::ReadProperty<'a, B> for ValidPacketSizes { - fn read(_buffer: &'a B) -> Self::Type { - () - } + fn read(_buffer: &'a B) -> Self::Type {} fn validate(buffer: &B) -> crate::result::Result<()> { use crate::buffer::UmpPrivate; if buffer.buffer().message().chunks_exact(4).any(|p| { @@ -85,6 +82,30 @@ impl<'a, B: crate::buffer::Ump> crate::detail::property::ReadProperty<'a, B> for } } +impl crate::detail::property::WriteProperty + for ValidPacketSizes +{ + fn write(buffer: &mut B, _: Self::Type) { + use crate::buffer::UmpPrivateMut; + + for packet in buffer + .buffer_mut() + .message_mut() + .chunks_exact_mut(4) + .take_while(|packet| u8::from(packet[0].nibble(0)) == UMP_MESSAGE_TYPE) + { + let sz = packet[0].nibble(3); + packet[0].set_nibble(3, sz.max(ux::u4::new(1))); + } + } + fn validate(_v: &Self::Type) -> crate::result::Result<()> { + Ok(()) + } + fn default() -> Self::Type { + Default::default() + } +} + struct GroupProperty; impl crate::detail::property::Property for GroupProperty { @@ -302,14 +323,17 @@ impl<'a> PayloadIterator<'a> { u8::from(packet[0].nibble(3)) as usize - 1 } fn finished(&self) -> bool { - self.data.len() / 4 == self.packet_index + self.size_cache == 0 + } + fn size_of_current_packet(&self) -> usize { + Self::packet_size(&self.data[self.packet_index * 4..(self.packet_index * 4 + 4)]) } fn advance(&mut self) { self.payload_index += 1; - self.size_cache -= 1; - if self.payload_index - == Self::packet_size(&self.data[self.packet_index * 4..(self.packet_index * 4 + 4)]) - { + if !self.finished() { + self.size_cache -= 1; + } + if self.payload_index >= self.size_of_current_packet() { // end of message self.packet_index += 1; self.payload_index = 0; @@ -329,7 +353,9 @@ impl Sysex for Sysex8 { data: self.0.buffer().message(), packet_index: 0, payload_index: 0, - size_cache: self.0.buffer().message()[..self.size()] + size_cache: self + .data() + .message() .chunks_exact(4) .map(|packet| PayloadIterator::packet_size(packet)) .sum(), @@ -494,7 +520,7 @@ mod tests { fn new() { assert_eq!( Sysex8::>::new(), - Sysex8(std::vec![0x0, 0x5000_0000, 0x0, 0x0, 0x0]) + Sysex8(std::vec![0x0, 0x5001_0000, 0x0, 0x0, 0x0]) ); } @@ -505,7 +531,7 @@ mod tests { let mut message = Sysex8::>::new(); message.set_group(ux::u4::new(0xC)); - assert_eq!(message, Sysex8(std::vec![0x0, 0x5C00_0000, 0x0, 0x0, 0x0])); + assert_eq!(message, Sysex8(std::vec![0x0, 0x5C01_0000, 0x0, 0x0, 0x0])); } #[test] @@ -996,12 +1022,51 @@ mod tests { #[test] fn set_payload_to_fixed_size_buffer_accidentally_missed_jr_header() { let mut message = Sysex8::<[u32; 16]>::try_new().unwrap(); - assert_eq!(message.try_set_payload(0..50), Err(crate::error::BufferOverflow)); + assert_eq!( + message.try_set_payload(0..50), + Err(crate::error::BufferOverflow) + ); } #[test] fn set_payload_to_fixed_size_buffer_with_overflow() { let mut message = Sysex8::<[u32; 17]>::try_new().unwrap(); - assert_eq!(message.try_set_payload(0..60), Err(crate::error::BufferOverflow)); + assert_eq!( + message.try_set_payload(0..60), + Err(crate::error::BufferOverflow) + ); + } + + #[test] + fn default_constructed_message() { + assert_eq!( + Sysex8::>::new(), + Sysex8(std::vec![0x0, 0x5001_0000, 0x0, 0x0, 0x0,]) + ); + } + + #[test] + fn payload_of_empty_message() { + let message = Sysex8::>::new(); + let payload = message.payload().collect::>(); + assert_eq!(payload, std::vec![]); + } + + #[test] + fn message_data_noop_jr_header() { + let mut message = Sysex8::>::new(); + let buffer: [u8; 0] = []; + message.set_payload(buffer.iter().cloned()); + assert_eq!( + Sysex8(std::vec![ + 0x0000_0000, + 0x5001_0000, + 0x0000_0000, + 0x0000_0000, + 0x0000_0000 + ]) + .data(), + &[0x5001_0000, 0x0000_0000, 0x0000_0000, 0x0000_0000], + ); } }