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
This commit is contained in:
@@ -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::<Value>(line), Ok(Value::Object(_)))
|
||||
matches!(serde_json::from_str::<Value>(strip_bom(line)), Ok(Value::Object(_)))
|
||||
}
|
||||
|
||||
// ─── take_string_field 辅助函数 ──────────────────────────────────────────────
|
||||
@@ -105,6 +118,8 @@ fn take_string_field(fields: &mut HashMap<String, Value>, keys: &[&str]) -> Opti
|
||||
// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。
|
||||
pub fn parse_line(line: &str) -> Option<LogEntry> {
|
||||
// ─── 跳过空行 ──────────────────────────────────────────────────────────
|
||||
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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user