如何在Rust中正确读取YAML文件中的二进制编码映射键?
我有一个用于编码和解码元素测试用例的YAML文件,左侧为预期编码字节,右侧为原始元素。以VarInt测试用例为例:
examples: "\0": 0 "\u0001": 1 "\u000A": 10 "\u00c8\u0001": 200 "\u00e8\u0007": 1000 "\u00a9\u0046": 9001 "\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u0001": -1
前三个示例的编码(左侧)作为字符串读取时能正常工作(Rust会自动将其解析为UTF-8),但第四个示例(对应200)及之后的示例无法得到正确结果。以200的编码"\u00c8\u0001"为例:
将编码作为UTF-8字符串读取(错误):
use bytes::{Buf, BufMut}; let encoding_as_utf8_string = "\u{00c8}\u{0001}"; // 输出:"È\u{1}" println!("Encoding as a UTF-8 string: {:?}", encoding_as_utf8_string); let mut utf8_bytes: &[u8] = encoding_as_utf8_string.as_bytes(); // 输出:[195, 136, 1](不正确) println!("Bytes obtained from the encoding when read as a UTF-8 string: {:?}", utf8_bytes);
将编码作为字节数组读取(正确):
use bytes::{Buf, BufMut}; let string_from_byte_array: String; unsafe { let encoding_as_byte_array: &[u8; 2] = b"\xc8\x01"; string_from_byte_array = String::from_utf8_unchecked(encoding_as_byte_array.to_vec()); } // 输出:"�" println!("Encoding string read from byte array: {:?}", string_from_byte_array); let mut bytes: &[u8] = string_from_byte_array.as_bytes(); // 输出:[200, 1](正确) println!("Bytes obtained from the encoding when read as a byte array: {:?}", bytes);
问题在于从YAML文件读取时,编码(映射键)会被自动解析为UTF-8字符串,导致原始字节丢失:
use serde::Deserialize; use serde_yaml::{Deserializer, Value}; let f = std::fs::read(yaml_dir).expect("Unable to read file"); for doc in Deserializer::from_slice(&f) { let spec = Value::deserialize(doc).expect("Unable to parse document"); // 输出:Mapping {..., "examples": Mapping {"\0": Number(0), "\u{1}": Number(1), "\n": Number(10), "È\u{1}": Number(200), "è\u{7}": Number(1000), "©F": Number(9001), "ÿÿÿÿÿÿÿÿÿ\u{1}": Number(-1)}} println!("YAML spec interpreted: {:?}", spec); }
使用serde_yaml的更具体示例:
// 输出:Sequence [Number(200), Number(1)](正确,但如何让YAML按此方式解析?) let bytes = serde_yaml::to_value(b"\xc8\x01").unwrap(); // 输出:String("È\u{1}")(不正确) let st = serde_yaml::to_value("\u{00c8}\u{0001}").unwrap();
我当前使用serde_yaml,但其他方法也可接受。需要解决两个问题:
- 如何让YAML中的编码(完全按写入形式)被正确解析为字节数组而非字符串?
- 或者,是否有办法继续将编码正常读取为UTF-8字符串,再从中提取原始非UTF-8字节?
方法一:修改YAML使用二进制类型(推荐)
YAML原生支持二进制数据类型,通过!!binary标签结合Base64编码,可以直接将原始字节序列存储为映射键,避免UTF-8解析问题。
修改后的YAML示例
将每个编码字节序列转换为Base64编码,并用!!binary标记:
examples: !!binary "AA==": 0 # 对应原"\0" !!binary "AQ==": 1 # 对应原"\u0001" !!binary "Cg==": 10 # 对应原"\u000A" !!binary "yA==": 200 # 对应原"\u00c8\u0001"(字节[0xc8, 0x01]) !!binary "6A==": 1000 # 对应原"\u00e8\u0007"(字节[0xe8, 0x07]) !!binary "qUY=": 9001 # 对应原"\u00a9\u0046"(字节[0xa9, 0x46]) !!binary "//////////w==": -1 # 对应原"\u00ff*9\u0001"(字节[0xff]*9 + [0x01])
Rust反序列化代码
定义一个结构体来接收测试用例,使用Vec<u8>作为映射键类型:
use serde::Deserialize; use serde_yaml; #[derive(Debug, Deserialize)] struct TestSpec { examples: std::collections::HashMap<Vec<u8>, i64>, } fn main() { let yaml_content = include_str!("test_cases.yaml"); let spec: TestSpec = serde_yaml::from_str(yaml_content).unwrap(); // 验证200的编码字节是否正确 assert_eq!(spec.examples.get(&vec![0xc8, 0x01]), Some(&200)); }
此方法直接从YAML解析出原始字节数组,完全避免UTF-8转换问题。
方法二:自定义Serde反序列化器解析Unicode转义
如果无法修改YAML文件,可以自定义反序列化逻辑,将YAML中解析出的字符串按Unicode转义序列(\uXXXX)直接转换为原始字节,忽略UTF-8合法性。
自定义反序列化实现
use serde::{Deserialize, Deserializer}; use serde_yaml; use std::collections::HashMap; // 自定义类型,用于存储原始字节序列 #[derive(Debug, PartialEq, Eq, Hash)] struct RawBytes(Vec<u8>); // 实现从字符串反序列化为RawBytes,解析\uXXXX转义为字节 impl<'de> Deserialize<'de> for RawBytes { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { use serde::de::Error; let s: String = String::deserialize(deserializer)?; let mut bytes = Vec::new(); let mut chars = s.chars().peekable(); while let Some(c) = chars.next() { if c == '\\' { // 处理转义序列 match chars.next() { Some('u') => { // 读取4个十六进制字符 let mut hex_str = String::with_capacity(4); for _ in 0..4 { hex_str.push(chars.next().ok_or_else(|| Error::custom("Incomplete \\u escape"))?); } // 转换为u8(直接取低8位,符合原YAML的字节定义) let byte = u8::from_str_radix(&hex_str, 16).map_err(Error::custom)?; bytes.push(byte); } Some('0') => bytes.push(0), Some('n') => bytes.push(b'\n'), // 可添加其他需要处理的转义序列 Some(other) => bytes.push(other as u8), None => return Err(Error::custom("Unexpected end of escape sequence")), } } else { // 非转义字符直接取UTF-8字节(适配原YAML中合法UTF-8的场景) bytes.extend_from_slice(c.to_string().as_bytes()); } } Ok(RawBytes(bytes)) } } #[derive(Debug, Deserialize)] struct TestSpec { examples: HashMap<RawBytes, i64>, } fn main() { let yaml_content = include_str!("original_test_cases.yaml"); let spec: TestSpec = serde_yaml::from_str(yaml_content).unwrap(); // 验证200的编码字节是否正确 assert_eq!(spec.examples.get(&RawBytes(vec![0xc8, 0x01])), Some(&200)); }
此方法不需要修改原YAML,通过自定义反序列化逻辑,将字符串中的\uXXXX转义直接解析为对应的单字节,还原原始编码。
方法三:不可行说明:从UTF-8字符串还原原始字节
此方法完全不可行,因为当YAML将\u00c8解析为UTF-8字符串时,原始字节0xc8会被转换为UTF-8的多字节序列0xc3 0x88,这一转换是不可逆的——无法从0xc3 0x88还原出原始的0xc8,UTF-8编码已经丢失了原始字节的信息。因此,此路径无法解决问题。
内容的提问来源于stack exchange,提问作者Nivaldo T

