Skip to content

Commit bd5088f

Browse files
committed
chore(release): Merge branch 'release/0.9.0'
2 parents 44e0b8f + c6bcd0c commit bd5088f

File tree

13 files changed

+2239
-36
lines changed

13 files changed

+2239
-36
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ repos:
55
- id: cargo-fmt
66
stages: [pre-commit]
77
- repo: https://github.com/compilerla/conventional-pre-commit
8-
rev: v4.0.0
8+
rev: v4.2.0
99
hooks:
1010
- id: conventional-pre-commit
1111
stages: [commit-msg]

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog
22

3+
## [0.9.0](https://github.com/midi2-dev/bl-midi2-rs/compare/0.8.2..0.9.0) - 2025-05-13
4+
5+
### ✨ Features
6+
7+
- Extend sysex api - ([909c6e1](https://github.com/midi2-dev/bl-midi2-rs/commit/909c6e171f838ccb2c3c16a78ffc94ede44ead70))
8+
- Payload from sysex packets - ([f196aac](https://github.com/midi2-dev/bl-midi2-rs/commit/f196aac7c91f6951038f710610fffd2b67da78b2))
9+
10+
### 🐛 Fixes
11+
12+
- Sysex7 packet with invalid size passes validation - ([edd5ca2](https://github.com/midi2-dev/bl-midi2-rs/commit/edd5ca278f653e0bdc3f516086dad4ed2502101e))
13+
- Ignore clippy lint with false positives - ([3f47e53](https://github.com/midi2-dev/bl-midi2-rs/commit/3f47e53529025d3deef5e5d42876375b8e6a5ed2))
14+
15+
### 🧪 Testing
16+
17+
- Adds sysex insert fuzz test - ([e0d5920](https://github.com/midi2-dev/bl-midi2-rs/commit/e0d592055c7fc3902d7c61155afb7368b517e841))
18+
19+
320
## [0.8.2](https://github.com/midi2-dev/bl-midi2-rs/compare/0.8.1..0.8.2) - 2025-03-02
421

522
### 🐛 Fixes

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ You'll want to setup midi2 without default features to compile
163163
without the `std` feature.
164164

165165
```toml
166-
midi2 = { version = "0.8.2", default-features = false, features = ["channel-voice2", "sysex7"], }
166+
midi2 = { version = "0.9.0", default-features = false, features = ["channel-voice2", "sysex7"], }
167167
```
168168

169169
### Generic Representation

fuzz/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ cargo-fuzz = true
99

1010
[dependencies]
1111
libfuzzer-sys = "0.4"
12+
arbitrary = { version = "1.0", features = [
13+
"derive",
14+
] }
15+
rand = "0.9.1"
1216

1317
[dependencies.midi2]
1418
path = "../midi2"
@@ -32,3 +36,10 @@ path = "./fuzz_targets/sysex7_payload_roundtrip.rs"
3236
test = false
3337
doc = false
3438
bench = false
39+
40+
[[bin]]
41+
name = "generic_sysex_inserting_payloads"
42+
path = "./fuzz_targets/generic_sysex_inserting_payloads.rs"
43+
test = false
44+
doc = false
45+
bench = false
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#![no_main]
2+
3+
use libfuzzer_sys::fuzz_target;
4+
use midi2::Sysex;
5+
use rand::{Rng, SeedableRng};
6+
7+
struct FixedSizeBuffer<U: midi2::buffer::Unit>(Vec<U>);
8+
9+
impl<U: midi2::buffer::Unit> midi2::buffer::Buffer for FixedSizeBuffer<U> {
10+
type Unit = U;
11+
fn buffer(&self) -> &[Self::Unit] {
12+
&self.0
13+
}
14+
}
15+
16+
impl<U: midi2::buffer::Unit> midi2::buffer::BufferMut for FixedSizeBuffer<U> {
17+
fn buffer_mut(&mut self) -> &mut [Self::Unit] {
18+
&mut self.0
19+
}
20+
}
21+
22+
impl<U: midi2::buffer::Unit> midi2::buffer::BufferTryResize for FixedSizeBuffer<U> {
23+
fn try_resize(&mut self, new_size: usize) -> Result<(), midi2::error::BufferOverflow> {
24+
if new_size > self.0.len() {
25+
return Err(midi2::error::BufferOverflow);
26+
}
27+
Ok(())
28+
}
29+
}
30+
31+
impl<U: midi2::buffer::Unit> FixedSizeBuffer<U> {
32+
fn new(size: usize) -> Self {
33+
Self(std::iter::repeat_n(U::zero(), size).collect())
34+
}
35+
}
36+
37+
#[derive(arbitrary::Arbitrary, Debug)]
38+
struct InputData {
39+
seed: u64,
40+
initial_data: Vec<u8>,
41+
data_to_insert: Vec<u8>,
42+
}
43+
44+
const MAX_BUFFER_SIZE: usize = 1024;
45+
46+
trait IntoByte<B> {
47+
fn byte(&self) -> B;
48+
}
49+
50+
impl IntoByte<midi2::ux::u7> for u8 {
51+
fn byte(&self) -> midi2::ux::u7 {
52+
midi2::num::u7::new(self & 0x7F)
53+
}
54+
}
55+
56+
impl IntoByte<u8> for u8 {
57+
fn byte(&self) -> u8 {
58+
*self
59+
}
60+
}
61+
62+
fn test_case<B, M>(data: &InputData, mut message: M, index: usize)
63+
where
64+
B: midi2::buffer::Buffer + midi2::buffer::BufferTryResize + midi2::buffer::BufferMut,
65+
M: midi2::Sysex<B>,
66+
<M as Sysex<B>>::Byte: Eq + core::fmt::Debug,
67+
u8: IntoByte<<M as Sysex<B>>::Byte>,
68+
{
69+
let Ok(()) = message.try_set_payload(data.initial_data.iter().map(u8::byte)) else {
70+
return;
71+
};
72+
73+
{
74+
let initial = message.payload().collect::<Vec<_>>();
75+
assert_eq!(
76+
initial,
77+
data.initial_data.iter().map(u8::byte).collect::<Vec<_>>()
78+
);
79+
}
80+
81+
let Ok(()) = message.try_insert_payload(data.data_to_insert.iter().map(u8::byte), index) else {
82+
return;
83+
};
84+
85+
let actual = message.payload().collect::<Vec<_>>();
86+
let expected = {
87+
let mut ret = data.initial_data.clone();
88+
ret.splice(index..index, data.data_to_insert.clone());
89+
ret.iter().map(u8::byte).collect::<Vec<_>>()
90+
};
91+
assert_eq!(actual, expected);
92+
}
93+
94+
fuzz_target!(|data: InputData| {
95+
let mut rng = rand::rngs::StdRng::seed_from_u64(data.seed);
96+
let fized_size_buffer_size = rng.random_range(4..MAX_BUFFER_SIZE);
97+
let index = if data.initial_data.is_empty() {
98+
0
99+
} else {
100+
rng.random_range(0..data.initial_data.len())
101+
};
102+
test_case(
103+
&data,
104+
midi2::sysex8::Sysex8::<FixedSizeBuffer<u32>>::try_new_with_buffer(
105+
FixedSizeBuffer::<u32>::new(fized_size_buffer_size),
106+
)
107+
.unwrap(),
108+
index,
109+
);
110+
test_case(
111+
&data,
112+
midi2::sysex7::Sysex7::<FixedSizeBuffer<u32>>::try_new_with_buffer(
113+
FixedSizeBuffer::<u32>::new(fized_size_buffer_size),
114+
)
115+
.unwrap(),
116+
index,
117+
);
118+
test_case(
119+
&data,
120+
midi2::sysex7::Sysex7::<FixedSizeBuffer<u8>>::try_new_with_buffer(
121+
FixedSizeBuffer::<u8>::new(fized_size_buffer_size),
122+
)
123+
.unwrap(),
124+
index,
125+
);
126+
});

midi2/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "midi2"
3-
version = "0.8.2"
3+
version = "0.9.0"
44
description = "Ergonomic, versatile, strong types wrapping MIDI 2.0 message data."
55
edition = "2021"
66
readme = "README.md"
@@ -10,10 +10,14 @@ authors = [
1010
]
1111
repository = "https://github.com/midi2-dev/bl-midi2-rs.git"
1212

13+
[lints.clippy]
14+
# packet indexing causes false positives
15+
manual_div_ceil = "allow"
16+
1317
[dependencies]
1418
derive_more = { version = "2.0.1", features = ["from"], default-features = false }
1519
fixed = "1.28.0"
16-
midi2_proc = { version = "0.8.2", path = "../midi2_proc" }
20+
midi2_proc = { version = "0.9.0", path = "../midi2_proc" }
1721
ux = "0.1.6"
1822

1923
[dev-dependencies]

midi2/src/detail/helpers.rs

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,16 @@ pub fn validate_sysex_group_statuses<
8989
Ok(())
9090
}
9191

92-
pub fn try_set_sysex_data<
92+
pub fn try_insert_sysex_data<
9393
B: crate::buffer::Buffer + crate::buffer::BufferMut + crate::buffer::BufferTryResize,
9494
S: SysexInternal<B>,
9595
D: core::iter::Iterator<Item = <S as crate::traits::Sysex<B>>::Byte>,
9696
>(
9797
sysex: &mut S,
9898
data: D,
99+
before: usize,
99100
) -> core::result::Result<(), crate::error::BufferOverflow> {
100-
match detail::try_set_sysex_data(sysex, data, |s, sz| s.try_resize(sz)) {
101+
match detail::try_insert_sysex_data(sysex, data, |s, sz| s.try_resize(sz), before) {
101102
Err(e) => {
102103
// if the write failed we reset the message
103104
// back to zero data
@@ -110,25 +111,33 @@ pub fn try_set_sysex_data<
110111
}
111112
}
112113

113-
pub fn set_sysex_data<
114+
pub fn insert_sysex_data<
114115
B: crate::buffer::Buffer + crate::buffer::BufferMut + crate::buffer::BufferResize,
115116
S: SysexInternal<B>,
116117
D: core::iter::Iterator<Item = <S as crate::traits::Sysex<B>>::Byte>,
117118
>(
118119
sysex: &mut S,
119120
data: D,
121+
before: usize,
120122
) {
121-
detail::try_set_sysex_data(sysex, data, |s, sz| {
122-
s.resize(sz);
123-
Ok(())
124-
})
123+
detail::try_insert_sysex_data(
124+
sysex,
125+
data,
126+
|s, sz| {
127+
s.resize(sz);
128+
Ok(())
129+
},
130+
before,
131+
)
125132
.expect("Resizable buffers should not fail here")
126133
}
127134

128135
mod detail {
136+
use crate::error::BufferOverflow;
137+
129138
use super::*;
130139

131-
pub fn try_set_sysex_data<
140+
pub fn try_insert_sysex_data<
132141
B: crate::buffer::Buffer + crate::buffer::BufferMut,
133142
S: crate::traits::SysexInternal<B>,
134143
D: core::iter::Iterator<Item = <S as crate::traits::Sysex<B>>::Byte>,
@@ -137,8 +146,14 @@ mod detail {
137146
sysex: &mut S,
138147
data: D,
139148
resize: R,
149+
before: usize,
140150
) -> core::result::Result<(), crate::error::BufferOverflow> {
151+
// reformat first to ensure data is optimally filling the
152+
// underlying buffer
153+
sysex.compact();
154+
141155
// get an initial estimate for the size of the data
156+
let initial_size = sysex.payload_size();
142157
let mut running_data_size_estimate = match data.size_hint() {
143158
(_, Some(upper)) => upper,
144159
// not the optimal case - could lead to additional copying
@@ -149,45 +164,62 @@ mod detail {
149164
let mut data = data.peekable();
150165

151166
// initial buffer resize
152-
if let Err(SysexTryResizeError(sz)) = resize(sysex, running_data_size_estimate) {
167+
if let Err(SysexTryResizeError(sz)) =
168+
resize(sysex, running_data_size_estimate + initial_size)
169+
{
153170
// failed. we'll work with what we've got
154-
running_data_size_estimate = sz;
171+
running_data_size_estimate = sz.saturating_sub(initial_size);
155172
};
156173

174+
debug_assert_eq!(
175+
sysex.payload_size(),
176+
running_data_size_estimate + initial_size
177+
);
178+
179+
let mut tail = before + running_data_size_estimate;
180+
sysex.move_payload_tail(before, tail);
181+
157182
'main: loop {
158183
while written < running_data_size_estimate {
159184
match data.next() {
160185
Some(v) => {
161-
sysex.write_datum(v, written);
186+
sysex.write_datum(v, before + written);
162187
written += 1;
163188
}
164189
None => {
165190
break 'main;
166191
}
167192
}
168193
}
169-
assert_eq!(written, running_data_size_estimate);
194+
debug_assert_eq!(written, running_data_size_estimate);
195+
196+
if data.peek().is_none() {
197+
// done
198+
break;
199+
}
170200

171201
// we underestimated.
172202
// resize to make more space
173203
running_data_size_estimate += additional_size_for_overflow;
204+
if let Err(SysexTryResizeError(sz)) =
205+
resize(sysex, running_data_size_estimate + initial_size)
206+
{
207+
// failed. we'll work with what we've got
208+
running_data_size_estimate = sz.saturating_sub(initial_size);
209+
};
210+
sysex.move_payload_tail(tail, before + running_data_size_estimate);
211+
tail = before + running_data_size_estimate;
174212
additional_size_for_overflow *= 2;
175213

176-
if data.peek().is_some() {
177-
if let Err(SysexTryResizeError(sz)) = resize(sysex, running_data_size_estimate) {
178-
// failed. we'll work with what we've got
179-
running_data_size_estimate = sz;
180-
};
181-
}
182-
183214
if written >= running_data_size_estimate {
184-
return Err(Default::default());
215+
return Err(BufferOverflow);
185216
}
186217
}
187218

188219
if written < running_data_size_estimate {
189220
// we shrink the buffer back down to the correct size
190-
resize(sysex, written).map_err(|_| crate::error::BufferOverflow)?;
221+
sysex.move_payload_tail(tail, before + written);
222+
resize(sysex, written + initial_size).map_err(|_| crate::error::BufferOverflow)?;
191223
}
192224

193225
Ok(())

0 commit comments

Comments
 (0)