@@ -139,6 +139,8 @@ impl Woolich {
139139 /// row to begin with an `HH:MM:SS.mmm` timestamp. No other supported
140140 /// format uses a `Log Time` header, so this is specific enough on its own.
141141 pub fn detect ( contents : & str ) -> bool {
142+ // Strip UTF-8 BOM if present (common on Windows CSV exports).
143+ let contents = contents. trim_start_matches ( '\u{FEFF}' ) ;
142144 let mut lines = contents. lines ( ) ;
143145
144146 let Some ( header) = lines. next ( ) else {
@@ -169,6 +171,8 @@ impl Woolich {
169171
170172impl Parseable for Woolich {
171173 fn parse ( & self , file_contents : & str ) -> Result < Log , Box < dyn Error > > {
174+ // Strip UTF-8 BOM if present (common on Windows CSV exports).
175+ let file_contents = file_contents. trim_start_matches ( '\u{FEFF}' ) ;
172176 let line_count = file_contents. lines ( ) . count ( ) ;
173177 let estimated_rows = line_count. saturating_sub ( 1 ) ;
174178
@@ -203,14 +207,18 @@ impl Parseable for Woolich {
203207 let mut times: Vec < f64 > = Vec :: with_capacity ( estimated_rows) ;
204208 let mut data: Vec < Vec < Value > > = Vec :: with_capacity ( estimated_rows) ;
205209 let mut first_time: Option < f64 > = None ;
210+ // Reuse one buffer for line splitting to avoid a heap allocation per
211+ // row (logs routinely run tens of thousands of rows).
212+ let mut fields: Vec < & str > = Vec :: with_capacity ( column_names. len ( ) ) ;
206213
207214 for line in lines {
208215 let line = line. trim ( ) ;
209216 if line. is_empty ( ) {
210217 continue ;
211218 }
212219
213- let fields: Vec < & str > = line. split ( ',' ) . collect ( ) ;
220+ fields. clear ( ) ;
221+ fields. extend ( line. split ( ',' ) ) ;
214222 let Some ( time_val) = fields. first ( ) . and_then ( |f| parse_log_time ( f) ) else {
215223 continue ;
216224 } ;
@@ -331,6 +339,18 @@ mod tests {
331339 assert_eq ! ( WoolichChannel :: from_header( "Clutch In" ) . unit, "" ) ;
332340 }
333341
342+ #[ test]
343+ fn handles_utf8_bom ( ) {
344+ // Windows CSV exports often prepend a UTF-8 BOM.
345+ let with_bom = format ! ( "\u{FEFF} {}" , SAMPLE ) ;
346+ assert ! ( Woolich :: detect( & with_bom) ) ;
347+
348+ let log = Woolich . parse ( & with_bom) . expect ( "should parse with BOM" ) ;
349+ assert_eq ! ( log. channels. len( ) , 8 ) ;
350+ assert_eq ! ( log. channels[ 0 ] . name( ) , "RPM" ) ;
351+ assert_eq ! ( log. data. len( ) , 3 ) ;
352+ }
353+
334354 #[ test]
335355 fn empty_file_fails_gracefully ( ) {
336356 assert ! ( Woolich . parse( "" ) . is_err( ) ) ;
0 commit comments