Skip to content

Commit 5bb78eb

Browse files
authored
Merge branch 'development' into feature/generic_ensemble
2 parents 5dcbf40 + 4c275d7 commit 5bb78eb

4 files changed

Lines changed: 492 additions & 205 deletions

File tree

src/dataset/mod.rs

Lines changed: 176 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ pub(crate) fn serialize_data<X: Number + RealNumber, Y: RealNumber>(
6262
) -> Result<(), io::Error> {
6363
match File::create(filename) {
6464
Ok(mut file) => {
65-
file.write_all(&dataset.num_features.to_le_bytes())?;
66-
file.write_all(&dataset.num_samples.to_le_bytes())?;
65+
// Write header as fixed-width u64 (little-endian) so the .xy files
66+
// can be read correctly on any target width, including wasm32.
67+
file.write_all(&(dataset.num_features as u64).to_le_bytes())?;
68+
file.write_all(&(dataset.num_samples as u64).to_le_bytes())?;
6769
let x: Vec<u8> = dataset
6870
.data
6971
.iter()
@@ -84,34 +86,123 @@ pub(crate) fn serialize_data<X: Number + RealNumber, Y: RealNumber>(
8486
Ok(())
8587
}
8688

89+
/// Deserialise a `.xy` dataset blob embedded via `include_bytes!`.
90+
///
91+
/// # Wire format
92+
/// ```text
93+
/// [u64 LE: num_features][u64 LE: num_samples]
94+
/// [f32 LE × (num_features * num_samples)] <- X matrix, row-major
95+
/// [f32 LE × num_samples] <- y vector
96+
/// ```
97+
///
98+
/// The header uses a **fixed 8-byte (u64) width** regardless of the host
99+
/// pointer size. Previous versions used `usize`, which is 4 bytes on
100+
/// `wasm32` but 8 bytes on x86-64 — meaning the `.xy` files (generated
101+
/// on x86-64) could not be parsed under WASM and every dataset test
102+
/// returned `data.len() == 0`.
87103
pub(crate) fn deserialize_data(
88104
bytes: &[u8],
89105
) -> Result<(Vec<f32>, Vec<f32>, usize, usize), io::Error> {
90-
// read the same file back into a Vec of bytes
91-
const USIZE_SIZE: usize = std::mem::size_of::<usize>();
106+
// Header: two u64 fields, each 8 bytes, platform-independent.
107+
const FIELD_SIZE: usize = std::mem::size_of::<u64>(); // always 8
108+
const HEADER_LEN: usize = 2 * FIELD_SIZE; // always 16
109+
110+
// Reject obviously-truncated buffers before reading any fields.
111+
if bytes.len() < HEADER_LEN {
112+
return Err(io::Error::new(
113+
io::ErrorKind::InvalidData,
114+
format!(
115+
"deserialize_data: buffer too small for header (need {HEADER_LEN} bytes, got {})",
116+
bytes.len()
117+
),
118+
));
119+
}
120+
92121
let (num_samples, num_features) = {
93-
let mut buffer = [0u8; USIZE_SIZE];
94-
buffer.copy_from_slice(&bytes[0..USIZE_SIZE]);
95-
let num_features = usize::from_le_bytes(buffer);
96-
buffer.copy_from_slice(&bytes[8..8 + USIZE_SIZE]);
97-
let num_samples = usize::from_le_bytes(buffer);
122+
let mut buf8 = [0u8; FIELD_SIZE];
123+
buf8.copy_from_slice(&bytes[0..FIELD_SIZE]);
124+
let num_features = u64::from_le_bytes(buf8) as usize;
125+
buf8.copy_from_slice(&bytes[FIELD_SIZE..HEADER_LEN]);
126+
let num_samples = u64::from_le_bytes(buf8) as usize;
98127
(num_samples, num_features)
99128
};
100129

101-
let mut x = Vec::with_capacity(num_samples * num_features);
130+
// Guard against integer overflow in num_samples * num_features.
131+
let num_x_values = num_samples.checked_mul(num_features).ok_or_else(|| {
132+
io::Error::new(
133+
io::ErrorKind::InvalidData,
134+
"deserialize_data: num_samples * num_features overflows usize",
135+
)
136+
})?;
137+
138+
// Validate the total byte length before any allocation.
139+
// Layout: HEADER_LEN + num_x_values * 4 + num_samples * 4
140+
let x_bytes = num_x_values.checked_mul(4).ok_or_else(|| {
141+
io::Error::new(
142+
io::ErrorKind::InvalidData,
143+
"deserialize_data: x byte range overflows usize",
144+
)
145+
})?;
146+
let y_bytes = num_samples.checked_mul(4).ok_or_else(|| {
147+
io::Error::new(
148+
io::ErrorKind::InvalidData,
149+
"deserialize_data: y byte range overflows usize",
150+
)
151+
})?;
152+
let expected_len = HEADER_LEN
153+
.checked_add(x_bytes)
154+
.and_then(|n| n.checked_add(y_bytes))
155+
.ok_or_else(|| {
156+
io::Error::new(
157+
io::ErrorKind::InvalidData,
158+
"deserialize_data: total expected length overflows usize",
159+
)
160+
})?;
161+
if bytes.len() < expected_len {
162+
return Err(io::Error::new(
163+
io::ErrorKind::InvalidData,
164+
format!(
165+
"deserialize_data: buffer too short (expected {expected_len} bytes, got {})",
166+
bytes.len()
167+
),
168+
));
169+
}
170+
171+
let mut x = Vec::with_capacity(num_x_values);
102172
let mut y = Vec::with_capacity(num_samples);
103173

104-
let mut buffer = [0u8; 4];
105-
let mut c = 16;
106-
for _ in 0..(num_samples * num_features) {
107-
buffer.copy_from_slice(&bytes[c..(c + 4)]);
108-
x.push(f32::from_bits(u32::from_le_bytes(buffer)));
174+
let mut buf4 = [0u8; 4];
175+
let mut c = HEADER_LEN;
176+
177+
for _ in 0..num_x_values {
178+
buf4.copy_from_slice(&bytes[c..(c + 4)]);
179+
let v = f32::from_bits(u32::from_le_bytes(buf4));
180+
if !v.is_finite() {
181+
return Err(io::Error::new(
182+
io::ErrorKind::InvalidData,
183+
format!(
184+
"deserialize_data: non-finite value in feature data (bits: {:#010x})",
185+
u32::from_le_bytes(buf4)
186+
),
187+
));
188+
}
189+
x.push(v);
109190
c += 4;
110191
}
111192

112-
for _ in 0..(num_samples) {
113-
buffer.copy_from_slice(&bytes[c..(c + 4)]);
114-
y.push(f32::from_bits(u32::from_le_bytes(buffer)));
193+
for _ in 0..num_samples {
194+
buf4.copy_from_slice(&bytes[c..(c + 4)]);
195+
let v = f32::from_bits(u32::from_le_bytes(buf4));
196+
if !v.is_finite() {
197+
return Err(io::Error::new(
198+
io::ErrorKind::InvalidData,
199+
format!(
200+
"deserialize_data: non-finite value in target data (bits: {:#010x})",
201+
u32::from_le_bytes(buf4)
202+
),
203+
));
204+
}
205+
y.push(v);
115206
c += 4;
116207
}
117208

@@ -144,4 +235,71 @@ mod tests {
144235
assert_eq!(m[0].len(), 5);
145236
assert_eq!(*m[1][3], 9);
146237
}
238+
239+
// deserialize_data unit tests — run on native AND wasm32.
240+
#[cfg_attr(
241+
all(target_arch = "wasm32", not(target_os = "wasi")),
242+
wasm_bindgen_test::wasm_bindgen_test
243+
)]
244+
#[test]
245+
fn deserialize_data_too_short() {
246+
let result = deserialize_data(&[0u8; 4]);
247+
assert!(result.is_err());
248+
}
249+
250+
#[cfg_attr(
251+
all(target_arch = "wasm32", not(target_os = "wasi")),
252+
wasm_bindgen_test::wasm_bindgen_test
253+
)]
254+
#[test]
255+
fn deserialize_data_truncated_body() {
256+
// Valid header (u64 LE): 1 feature, 1 sample — but no payload bytes.
257+
// Header is 16 bytes; expected total = 16 + 4 (x) + 4 (y) = 24.
258+
let mut buf = vec![0u8; 16];
259+
buf[0..8].copy_from_slice(&1u64.to_le_bytes()); // num_features = 1
260+
buf[8..16].copy_from_slice(&1u64.to_le_bytes()); // num_samples = 1
261+
let result = deserialize_data(&buf);
262+
assert!(result.is_err());
263+
}
264+
265+
#[cfg_attr(
266+
all(target_arch = "wasm32", not(target_os = "wasi")),
267+
wasm_bindgen_test::wasm_bindgen_test
268+
)]
269+
#[test]
270+
fn deserialize_data_nan_rejected() {
271+
// Construct a valid 1×1 dataset where the feature value is NaN.
272+
let nan_bits: u32 = f32::NAN.to_bits();
273+
let mut buf = vec![0u8; 16 + 4 + 4];
274+
buf[0..8].copy_from_slice(&1u64.to_le_bytes()); // num_features = 1
275+
buf[8..16].copy_from_slice(&1u64.to_le_bytes()); // num_samples = 1
276+
buf[16..20].copy_from_slice(&nan_bits.to_le_bytes()); // x[0] = NaN
277+
buf[20..24].copy_from_slice(&1.0f32.to_le_bytes()); // y[0] = 1.0
278+
let result = deserialize_data(&buf);
279+
assert!(result.is_err());
280+
}
281+
282+
/// Smoke-test that a correctly-formed 1×1 round-trip parses on every
283+
/// target width, including wasm32.
284+
#[cfg_attr(
285+
all(target_arch = "wasm32", not(target_os = "wasi")),
286+
wasm_bindgen_test::wasm_bindgen_test
287+
)]
288+
#[test]
289+
fn deserialize_data_roundtrip_1x1() {
290+
let x_val = 3.14f32;
291+
let y_val = 1.0f32;
292+
let mut buf = vec![0u8; 16 + 4 + 4];
293+
buf[0..8].copy_from_slice(&1u64.to_le_bytes()); // num_features = 1
294+
buf[8..16].copy_from_slice(&1u64.to_le_bytes()); // num_samples = 1
295+
buf[16..20].copy_from_slice(&x_val.to_bits().to_le_bytes());
296+
buf[20..24].copy_from_slice(&y_val.to_bits().to_le_bytes());
297+
let (x, y, ns, nf) = deserialize_data(&buf).expect("roundtrip must succeed");
298+
assert_eq!(ns, 1);
299+
assert_eq!(nf, 1);
300+
assert_eq!(x.len(), 1);
301+
assert_eq!(y.len(), 1);
302+
assert!((x[0] - x_val).abs() < 1e-6);
303+
assert!((y[0] - y_val).abs() < 1e-6);
304+
}
147305
}

0 commit comments

Comments
 (0)