Skip to content

Commit d7a4167

Browse files
fix(woolich): address PR #71 review feedback
- Strip UTF-8 BOM in detect() and parse() for Windows CSV exports - Reuse a single field buffer across rows to avoid per-row allocations - Add UTF-8 BOM regression test
1 parent c501fea commit d7a4167

1 file changed

Lines changed: 21 additions & 1 deletion

File tree

src/parsers/woolich.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

170172
impl 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

Comments
 (0)