You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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,但其他方法也可接受。需要解决两个问题:

  1. 如何让YAML中的编码(完全按写入形式)被正确解析为字节数组而非字符串?
  2. 或者,是否有办法继续将编码正常读取为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 06:20:41