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)。
|
// types 模块中定义了 LogEntry(一条日志记录)和 LogLevel(日志级别,如 INFO/ERROR)。
|
||||||
use crate::types::{LogEntry, LogLevel};
|
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 函数 ──────────────────────────────────────────────────
|
// ─── detect_json_log 函数 ──────────────────────────────────────────────────
|
||||||
// 检测一行文本是否是一个 JSON 对象。
|
// 检测一行文本是否是一个 JSON 对象。
|
||||||
//
|
//
|
||||||
@@ -63,7 +76,7 @@ pub fn detect_json_log(line: &str) -> bool {
|
|||||||
// 则匹配成功。_ 是通配符,表示"不关心对象里面的具体内容"。
|
// 则匹配成功。_ 是通配符,表示"不关心对象里面的具体内容"。
|
||||||
//
|
//
|
||||||
// 如果匹配到 Ok(Value::Object(_)) 返回 true,否则返回 false。
|
// 如果匹配到 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 辅助函数 ──────────────────────────────────────────────
|
// ─── take_string_field 辅助函数 ──────────────────────────────────────────────
|
||||||
@@ -105,6 +118,8 @@ fn take_string_field(fields: &mut HashMap<String, Value>, keys: &[&str]) -> Opti
|
|||||||
// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。
|
// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。
|
||||||
pub fn parse_line(line: &str) -> Option<LogEntry> {
|
pub fn parse_line(line: &str) -> Option<LogEntry> {
|
||||||
// ─── 跳过空行 ──────────────────────────────────────────────────────────
|
// ─── 跳过空行 ──────────────────────────────────────────────────────────
|
||||||
|
let line = strip_bom(line);
|
||||||
|
|
||||||
// line.trim() 去除首尾空白字符(空格、制表符、换行符等)。
|
// line.trim() 去除首尾空白字符(空格、制表符、换行符等)。
|
||||||
// .is_empty() 检查是否为空字符串。
|
// .is_empty() 检查是否为空字符串。
|
||||||
// 如果去除空白后是空的,说明是空行,不需要解析,直接返回 None。
|
// 如果去除空白后是空的,说明是空行,不需要解析,直接返回 None。
|
||||||
@@ -403,4 +418,42 @@ mod tests {
|
|||||||
// "time" 已被提取并从 fields 中移除。
|
// "time" 已被提取并从 fields 中移除。
|
||||||
assert!(entry.fields.get("time").is_none());
|
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