#deserialize #serde #binary #macro-derive #paradox #jomini #jomini-deserialize

jomini_derive

Serde宏实现,用于实现以下功能:#[derive(JominiDeserialize)]

7个版本

0.2.4 2024年2月28日
0.2.3 2023年11月22日
0.2.2 2021年9月8日
0.2.1 2020年8月15日
0.1.2 2020年8月6日

#6 in #paradox

Download history 151/week @ 2024-04-18 141/week @ 2024-04-25 167/week @ 2024-05-02 153/week @ 2024-05-09 128/week @ 2024-05-16 125/week @ 2024-05-23 162/week @ 2024-05-30 152/week @ 2024-06-06 170/week @ 2024-06-13 157/week @ 2024-06-20 135/week @ 2024-06-27 134/week @ 2024-07-04 129/week @ 2024-07-11 126/week @ 2024-07-18 136/week @ 2024-07-25 115/week @ 2024-08-01

521 每月下载量
jomini 中使用

MIT 许可证

23KB
462

ci Version

Jomini

Paradox Development Studio游戏(例如:欧洲全史(EU4)、钢铁雄心(HOI4)、国王之路(CK3)、Imperator、Stellaris和维多利亚)的保存和游戏文件的低级、性能导向解析器。

要深入了解Paradox Clausewitz格式以及尝试支持所有变体时可能遇到的问题,请参阅这篇介绍。简而言之,要编写一个健壮且快速的解析器,同时抽象出游戏之间以及游戏补丁之间的格式差异,是非常困难的。Jomini在灵活性和易用性之间找到了一个完美的平衡点。

Jomini是在线EU4保存文件分析器的基石。这个库还支持Paradox游戏转换器pdxu

功能

  • ✔ 通用:处理明文和二进制编码的数据
  • ✔ 快速:以超过1 GB/s的速度解析数据
  • ✔ 小巧:编译时无依赖
  • ✔ 安全:广泛针对潜在恶意输入进行模糊测试
  • ✔ 易用:使用类似serde的宏来自动实现解析逻辑
  • ✔ 可嵌入:跨平台原生应用、静态编译的服务或在浏览器中通过Wasm

快速入门

以下是一个使用serde反序列化明文数据的示例。还使用了几个额外的类似serde的属性来将serde数据模型与这些文件的结构相协调。

use jomini::{
    text::{Operator, Property},
    JominiDeserialize,
};

#[derive(JominiDeserialize, PartialEq, Debug)]
pub struct Model {
    human: bool,
    first: Option<u16>,
    third: Property<u16>,
    #[jomini(alias = "forth")]
    fourth: u16,
    #[jomini(alias = "core", duplicated)]
    cores: Vec<String>,
    names: Vec<String>,
    #[jomini(take_last)]
    checksum: String,
}

let data = br#"
    human = yes
    third < 5
    forth = 10
    core = "HAB"
    names = { "Johan" "Frederick" }
    core = FRA
    checksum = "first"
    checksum = "second"
"#;

let expected = Model {
    human: true,
    first: None,
    third: Property::new(Operator::LessThan, 5),
    fourth: 10,
    cores: vec!["HAB".to_string(), "FRA".to_string()],
    names: vec!["Johan".to_string(), "Frederick".to_string()],
    checksum: "second".to_string(),
};

let actual: Model = jomini::text::de::from_windows1252_slice(data)?;
assert_eq!(actual, expected);

二进制反序列化

以类似的方式反序列化二进制格式的数据,但调用者需要提供一些额外的步骤。

  • 文本应该如何解码(通常是Windows-1252或UTF-8)
  • 如何解码有理数(浮点数)
  • 如何解析令牌,这些令牌是16位整数,可以唯一地标识字符串

请注意,Paradox游戏有不同的二进制格式,并且补丁之间的二进制格式也可能不同!

以下是一个示例,它定义了一个示例二进制格式并使用哈希表令牌查找。

use jomini::{Encoding, JominiDeserialize, Windows1252Encoding, binary::BinaryFlavor};
use std::{borrow::Cow, collections::HashMap};

#[derive(JominiDeserialize, PartialEq, Debug)]
struct MyStruct {
    field1: String,
}

#[derive(Debug, Default)]
pub struct BinaryTestFlavor;

impl jomini::binary::BinaryFlavor for BinaryTestFlavor {
    fn visit_f32(&self, data: [u8; 4]) -> f32 {
        f32::from_le_bytes(data)
    }

    fn visit_f64(&self, data: [u8; 8]) -> f64 {
        f64::from_le_bytes(data)
    }
}

impl Encoding for BinaryTestFlavor {
    fn decode<'a>(&self, data: &'a [u8]) -> Cow<'a, str> {
        Windows1252Encoding::decode(data)
    }
}

let data = [ 0x82, 0x2d, 0x01, 0x00, 0x0f, 0x00, 0x03, 0x00, 0x45, 0x4e, 0x47 ];

let mut map = HashMap::new();
map.insert(0x2d82, "field1");

let actual: MyStruct = BinaryTestFlavor.deserialize_slice(&data[..], &map)?;
assert_eq!(actual, MyStruct { field1: "ENG".to_string() });

当正确完成时,可以使用相同的结构来表示明文和二进制数据,而不重复。

可以配置当令牌未知时的行为(例如:立即失败或尝试继续)。

使用token属性进行直接标识符反序列化

在二进制反序列化过程中可能会出现一些性能损失,因为令牌通过TokenResolver解析为字符串,然后与结构体字段的字符串表示形式进行匹配。

我们可以通过直接将预期的令牌值编码到结构体中解决这个问题

#[derive(JominiDeserialize, PartialEq, Debug)]
struct MyStruct {
    #[jomini(token = 0x2d82)]
    field1: String,
}

// Empty token to string resolver
let map = HashMap::<u16, String>::new();

let actual: MyStruct = BinaryDeserializer::builder_flavor(BinaryTestFlavor)
    .deserialize_slice(&data[..], &map)?;
assert_eq!(actual, MyStruct { field1: "ENG".to_string() });

一些注意事项

  • 这并不意味着不需要令牌到字符串解析器,因为令牌可能用作值。
  • 如果结构体中的一个字段指定了token属性,则必须在该结构体的所有字段上指定该属性。

注意事项

在调用任何Jomini API之前,调用者应该

  • 提前确定正确的格式(文本或二进制)。
  • 去除任何可能存在的标题(例如:EU4txt / EU4bin
  • 提供二进制格式的令牌解析器
  • 提供转换,以解决例如,日期在二进制格式中可能编码为整数,但在明文中为字符串的情况。

中级API

如果自动反序列化通过JominiDeserialize太高层次,则有一个中级API,可以轻松迭代解析的文档并调查字段的信息。

use jomini::TextTape;

let data = b"name=aaa name=bbb core=123 name=ccc name=ddd";
let tape = TextTape::from_slice(data).unwrap();
let reader = tape.windows1252_reader();

for (key, _op, value) in reader.fields() {
    println!("{:?}={:?}", key.read_str(), value.read_str().unwrap());
}

对于更低级别的解析,请参阅各自的二进制和文本文档。

中级API还提供了当启用json功能时将明文Clausewitz格式转换为JSON的出色功能。

use jomini::TextTape;

let tape = TextTape::from_slice(b"foo=bar")?;
let reader = tape.windows1252_reader();
let actual = reader.json().to_string()?;
assert_eq!(actual, r#"{"foo":"bar"}"#);

编写API

对于写入API有两个目标用例。一个是当手头有文本磁带时。这有助于在需要重新格式化文档时使用(请注意,注释不会被保留)

use jomini::{TextTape, TextWriterBuilder};

let tape = TextTape::from_slice(b"hello   = world")?;
let mut out: Vec<u8> = Vec::new();
let mut writer = TextWriterBuilder::new().from_writer(&mut out);
writer.write_tape(&tape)?;
assert_eq!(&out, b"hello=world");

编写器标准化任何格式问题。编写器无法无损地写入所有解析的文档,但这些仅限于真正古怪的情况,并希望在未来的版本中得到解决。

另一个用例更多地针对增量写入,这在熔合器或手工制作文档的人中可以找到。这些用例需要手动驱动编写器

use jomini::TextWriterBuilder;
let mut out: Vec<u8> = Vec::new();
let mut writer = TextWriterBuilder::new().from_writer(&mut out);
writer.write_unquoted(b"hello")?;
writer.write_unquoted(b"world")?;
writer.write_unquoted(b"foo")?;
writer.write_unquoted(b"bar")?;
assert_eq!(&out, b"hello=world\nfoo=bar");

不支持的语法

由于Clausewitz是闭源,这个库永远不能保证与Clausewitz兼容。没有关于有效输入的规范,我们只有一些在野外收集的例子。据我们所知,Clausewitz非常灵活:允许每个游戏对象定义自己的独特语法。

我们只能尽力而为,并在遇到新语法时添加对它的支持。

基准测试

基准测试是用以下命令运行的

cargo clean
cargo bench -- parse
find ./target -wholename "*/new/raw.csv" -print0 | xargs -0 xsv cat rows > assets/jomini-benchmarks.csv

可以使用位于资产目录中的R脚本进行分析。

以下是针对任意计算机进行基准测试生成的图表。

jomini-bench-throughput.png

依赖关系

~1.5MB
~35K SLoC