@@ -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`.
87103pub ( 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