From eedab3ac96e460303b4d0ee2510b89503792c336 Mon Sep 17 00:00:00 2001 From: dailz Date: Wed, 10 Jun 2026 11:37:00 +0800 Subject: [PATCH] fix(parser): strip UTF-8 BOM before JSON parsing serde_json rejects BOM (U+FEFF) prefixed input with 'expected value' error, causing BOM-prefixed JSON log lines to be silently dropped. Add strip_bom() helper that strips exactly one leading BOM character, apply it in detect_json_log() and parse_line(). Closes #20 --- crates/core/src/parser/json.rs | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/core/src/parser/json.rs b/crates/core/src/parser/json.rs index 5f974d1..0e189a5 100644 --- a/crates/core/src/parser/json.rs +++ b/crates/core/src/parser/json.rs @@ -34,6 +34,19 @@ use serde_json::Value; // types 模块中定义了 LogEntry(一条日志记录)和 LogLevel(日志级别,如 INFO/ERROR)。 use crate::types::{LogEntry, LogLevel}; +// ─── strip_bom 辅助函数 ────────────────────────────────────────────────── +// 剥离行首的 UTF-8 BOM(Byte Order Mark, U+FEFF)。 +// +// Windows 环境和某些导出工具生成的文件会在行首插入 BOM, +// 而 serde_json 不接受 BOM 前缀的 JSON 文本(会报 "expected value" 错误)。 +// 只剥离一个前导 BOM,不处理多个 BOM 或行内 BOM(那些是畸形输入)。 +// +// 参数: line: &str — 输入字符串切片。 +// 返回: &str — 去掉 BOM 后的字符串切片(借用原始字符串,零分配)。 +fn strip_bom(line: &str) -> &str { + line.strip_prefix('\u{FEFF}').unwrap_or(line) +} + // ─── detect_json_log 函数 ────────────────────────────────────────────────── // 检测一行文本是否是一个 JSON 对象。 // @@ -63,7 +76,7 @@ pub fn detect_json_log(line: &str) -> bool { // 则匹配成功。_ 是通配符,表示"不关心对象里面的具体内容"。 // // 如果匹配到 Ok(Value::Object(_)) 返回 true,否则返回 false。 - matches!(serde_json::from_str::(line), Ok(Value::Object(_))) + matches!(serde_json::from_str::(strip_bom(line)), Ok(Value::Object(_))) } // ─── take_string_field 辅助函数 ────────────────────────────────────────────── @@ -105,6 +118,8 @@ fn take_string_field(fields: &mut HashMap, keys: &[&str]) -> Opti // Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。 pub fn parse_line(line: &str) -> Option { // ─── 跳过空行 ────────────────────────────────────────────────────────── + let line = strip_bom(line); + // line.trim() 去除首尾空白字符(空格、制表符、换行符等)。 // .is_empty() 检查是否为空字符串。 // 如果去除空白后是空的,说明是空行,不需要解析,直接返回 None。 @@ -403,4 +418,42 @@ mod tests { // "time" 已被提取并从 fields 中移除。 assert!(entry.fields.get("time").is_none()); } + + #[test] + fn test_bom_prefixed_json() { + let line = "\u{FEFF}{\"level\":\"INFO\",\"message\":\"hello\"}"; + let entry = parse_line(line).unwrap(); + assert_eq!(entry.level, Some(LogLevel::Info)); + assert_eq!( + entry.fields.get("message"), + Some(&Value::String("hello".into())) + ); + } + + #[test] + fn test_bom_prefixed_detect() { + assert!(detect_json_log("\u{FEFF}{\"level\":\"INFO\"}")); + } + + #[test] + fn test_bom_only_whitespace() { + assert!(parse_line("\u{FEFF} ").is_none()); + } + + #[test] + fn test_bom_stripped_from_raw_line() { + let line = "\u{FEFF}{\"level\":\"INFO\",\"message\":\"hello\"}"; + let entry = parse_line(line).unwrap(); + assert_eq!(entry.raw_line, "{\"level\":\"INFO\",\"message\":\"hello\"}"); + } + + #[test] + fn test_internal_bom_not_stripped() { + let line = "{\"message\":\"\u{FEFF}hello\"}"; + let entry = parse_line(line).unwrap(); + assert_eq!( + entry.fields.get("message"), + Some(&Value::String("\u{FEFF}hello".into())) + ); + } }