4 个版本 (有破坏性)

0.4.0 2022年7月5日
0.3.0 2021年4月15日
0.2.0 2021年3月29日
0.1.0 2021年3月29日

#1057 in 编码

MIT 许可证

305KB
6K SLoC

Neodyn Exchange 格式规范

Neodyn Exchange on crates.io Neodyn Exchange on docs.rs Minimum Supported Rust Version (MSRV) Downloads License Lines of Code

Neodyn Exchange 是从 Serde 数据模型衍生出来的序列化格式。它被设计成易于使用、便携且高效的系统间数据交换方式。虽然参考实现是在 Rust 中,但鼓励在其他语言中实现。

使用快速入门

像许多 Serde 兼容的序列化格式一样,Neodyn Exchange 主要通过调用不同格式的顶层函数来使用。包根重新导出以下函数

// For serialization:
fn to_value()           // serializes to an in-memory, loosely-typed `Value`
fn to_string()          // serializes to a string, in the text representation
fn to_bytes()           // serializes to a Vec<u8>, in the binary format
fn to_writer()          // serializes to an io::Write, in the binary format
fn to_writer_buffered() // same as to_writer(), but uses buffering

// For deserialization
fn from_value()           // deserializes from an in-memory, loosely-typed Value
fn from_value_ref()       // same as above, but takes a borrowed &Value instead
fn from_str()             // deserializes from the text representation
fn from_bytes()           // deserializes from a byte slice in the binary format
fn from_reader()          // deserializes from an io::Read in the binary format
fn from_reader_buffered() // same as above, but uses buffering for efficiency

在传统命名的 serde 模块中还有更多函数,但这些函数较少见(例如,将文本格式写入原始字节)。

Value 枚举也导出到顶层;它代表 Neodyn Exchange 格式可以处理的所有可能值。此外,Value&Value 实现了 Deserializer 特性,以及 SerializeDeserialize,因此您可以在某些类型或其他格式在序列化/反序列化实现中不够灵活的情况下,将它们用作最后的逃生舱。

此外,实现 SerializerDeserializer 的类型也是公共的(然而,它们不会被重新导出)。但是,它们有一些注意事项(例如,其中一些必须手动调用 .finalize()),请参阅它们各自的文档以获取更多信息。

表示形式

Neodyn Exchange 数据模型有三个等效的表示形式

  1. 一个由 Rust 参考实现中的 Value 类型实现的抽象、结构化、内存中的值树;
  2. 一种人类可读的、格式化的文本格式;
  3. 以及一种机器可读的、紧凑的二进制格式。

这些表示法都可以存储相同的一组值。具体如何解释序列化值可能在不同的应用程序中有所不同。例如,由于解析每种表示法的方式各有不同,Serde参考实现可能在反序列化结构化、可读和二进制格式时,在不同时间点产生不同类型的错误。

类型系统

Neodyn交换是一种自描述的格式,因此每个序列化值都携带其类型。这也意味着,只要对解析器输出的松散类型值树感到满意,就不需要模式来解码序列化值。

可能的数据类型如下

  1. null,表示可选值的缺失。
  2. opt,表示存在一个(包装的)可选值。
  3. bool,一个普通的布尔值,可以是truefalse
  4. int,64位有符号整数。
  5. uint,64位无符号整数。
  6. float,64位IEEE-754浮点数,但不能是NaN
  7. string,UTF-8编码的、可读的文本。
  8. blob,任意原始字节序列。
  9. array,任意嵌套值的有序集合。
  10. map,键值对的未排序集合,其中键和值都是任意嵌套的Neodyn交换值。

抽象值树表示基本上是一对一反映这个系统。

注意:有符号整数、无符号整数和浮点类型统称为数字。(特别是要注意,bool不被视为数字。)

文本表示

文本表示定义为UTF-8编码的字符串。以下是非正式的(即:非严格的)、BNF风格的语法。

第一个标记之前、最后一个标记之后以及连续标记之间的空白(根据Unicode定义)是被允许的,并且被忽略。例外是字符串,字符串被视为单个标记,并且字符串中字符之间的空白不应被忽略。(这只是常规语义,因为我们当然希望保留字符串内的空白。)

以下符号的详细说明

  • 非终结符号生产用全部大写字母书写,不加引号,例如 VALUE
  • 生产定义用:=表示,例如 BOOL := 'true' | 'false'
  • 文本字面量用单引号括起来,例如 'null'
  • 无序选择用|表示,例如 foo | bar
  • 圆括号用于分组,例如 (VALUE)
  • Kleene 运算符 ?*+ 分别表示可选(0或1次重复)、任意(0或多次重复)和多次(1或多次重复)的项目,例如:(KEY ':' VALUE ',')*
  • Unicode 代码点的范围使用 PCRE 表示法书写,例如:[0-9]
  • 连续字符范围内的固定次数重复也用 PCRE 表示法书写,例如:[0-9a-fA-F]{2}
  • Unicode 概念和其他可读性强的简化描述用尖括号给出,例如:<XID_continue>
VALUE := NULL | OPT | BOOL | INT | UINT | FLOAT | STRING | BLOB | ARRAY | MAP

NULL := 'null' WB
OPT := '?' VALUE

BOOL := ('true' | 'false') WB

INT := ('+' | '-') [0-9]+ WB
UINT := [0-9]+ WB
FLOAT := ('+' | '-')? (([0-9]* '.' [0-9]+) | ([0-9]+ '.' [0-9]*) | 'inf') WB

STRING := '"' ( UNESCAPED | ESCAPED )* '"'
UNESCAPED := <any Unicode codepoint except '"' and '\'>
ESCAPED := '\n' | '\r' | '\t' | '\\' | '\'' | '\"' | '\u{[0-9a-fA-F]+}'

BLOB := '#' ([0-9a-fA-F]{2})* '#'

ARRAY := '[' ( VALUE ( ',' VALUE )* ','? )? ']'
MAP := '{' ( PAIR ( ',' PAIR )* ','? )? '}'
PAIR := VALUE ':' VALUE

WB := <Unicode word boundary or ASCII punctuation>

注意

  • 可读性格式是区分大小写的。所有内置字面量词都是用 小写 写的。(字符串的大小写当然被保留。)

  • 所有数字都写成十进制。可读性格式不允许使用除了10以外的基数。

  • 数字和单词(例如字面量 nulltruefalseinf)必须由 Unicode 词边界或 ASCII 标点符号与其周围隔开。这种约束的目的是不允许对人类读者难以解析的字符串,例如 123null 不应被接受为两个有效标记 123null,因为它们在视觉上没有分开。

  • 有符号整数和无符号整数之间有区别,即 +4242 不相同。这意味着文本格式可以保留正有符号整数的类型,它不同于无符号整数。

  • 浮点数必须在至少一个十进制点的一侧有数字。规范地,它们至少在十进制点两侧都打印至少一个数字,即使那个数字碰巧是零,例如 -0.3+12.0。浮点数的符号是可选的,但在规范表示法中始终打印。

  • 浮点数能够表示正负无穷大,并保留零的符号,但禁止使用 NaN。Neodyn 交换格式的三种表示法中都将 NaN 值转换为 null。但是,这意味着 NaN 在序列化然后反序列化时不会往返,因为尝试将 null 反序列化为浮点数被视为错误。

  • 所有三种数字类型都允许并忽略前导零,但浮点数中的 inf 除外。浮点数小数部分中的尾随零也允许并忽略。

    规范表示法是不打印任何前导或尾随零,除非数字(对于整数)或其整数或小数部分(对于浮点数)等于零。在这些情况下,每个需要的地方打印一个确切的零位数字。

  • 数组和映射中的尾随逗号允许并忽略。规范格式是始终打印尾随逗号。

  • 在 blob 字面量中的十六进制数字对之间允许并忽略空白。但十六进制数字对(即表示同一字节的最高和最低四位)内的十六进制数字 必须不 被空白分隔。

  • 在 blob 字面量和 Unicode 转义序列中的十六进制数字 不区分大小写,小写和大写字符 a...fA...F 是成对等价的。规范表示法是使用小写。

  • 字符串字面量可以包含“原始”换行符和制表符。这些被解释为原样。规范表示法是始终转义换行符和制表符。

  • 转义序列的解释遵循 Rust 的规则。这意味着大多数情况下采用通常的解释(例如,\n 表示换行,\t 是制表符等),并增加了 \u{...} 的解释,其中 ... 表示一系列连续的十六进制数字(不区分大小写),它被解析为一个 Unicode 代码点。允许前导零,并且规范表示法是不打印任何前导零。

文本表示示例

[
  {
    +39: -.354,
    -1.: true,
    +3.142: -6.283,
    0: null,
    1: ?"an optional string",
    2: ??"two levels of optionals; even an optional null is allowed, e.g.:",
    null: ?null,
    "as you can see": "null is allowed to be a key as well",
    "escaped\nnewline": "unescaped
newline",
    ["arrays","and","maps"]:{"can":"be","keys":"too"},
    "this is a map": "with a trailing comma",
  },
  {
    "optional array": ?[
      "first",
      "second",
    ],
    "empty map": {},
    "array without a trailing comma": [1, 2, 3],
    "this is a map": "also without a trailing comma"
  },
]

二进制表示

二进制格式试图将值压缩到尽可能小的空间。然而,它是一个严格按字节对齐的格式,即它操作的是至少 8 位的单位,并假定 8 位字节。它采用诸如可变宽度整数编码和字符串内联等技术,以实现小的序列化对象大小。因此,虽然概念上的类型系统描述了 64 位整数和浮点数,但足够低阶的值和/或足够少的有效数字实际上在编码时可能使用更少的空间。

类型标记和可变宽度编码

每个值都以一个包含至少类型信息的一个字节标记开始,但通常还包含其他数据,如长度或内联小值。

类型是有层次组织的。有三个级别的类型

  1. 每个值都有一个 主要类型。
  2. 对于某些主要类型,有一个或多个 次要类型。
  3. 对于某些主要和次要类型的组合,有一个 值标记。

类型注解和这些附加信息在字节中占据定义良好的位置

  • 主要类型存储在 3 个最高有效位(#5-#7)。
  • 如果有,次要类型存储在之前的 3 个位,即位 #2-#4。
  • 如果有,值标记占用 2 个最低有效位,#0-#1。
  • 某些主要-次要组合在较低的两个位中存储一个 2 位的 对数长度 信息,而不是值标记。这表示后续有 20=1、21=2、22=4 或 23=8 字节的其他数据。这通常是整数或浮点数,始终以小端字节顺序。
  • 还有一些其他主要类型省略了次要类型以及值标记,并在较低 5 位(#0-#4)中存储一个 小整数有效负载

总之,类型/值标记的可能格式是

  • 主要、次要、值标记:XXX YYY ZZ,(X = 主要位,Y = 次要位,Z = 值标记位)
  • 主版本,次版本,日志长度:XXX YYY NN(X = 主版本位,Y = 次版本位,N = 数据长度2的日志的位数)
  • 主版本,小有效载荷:XXX VVV VV(X = 主版本位,V = 有效载荷数据位)

序列化值的结构。符号表。

序列化的镱交换值在二进制格式中由两部分组成

  1. 头部或“符号表”
  2. 主体

字符串和二进制数据块以压缩方式存储:每个相同的字节数组只写一次。这些唯一的字节数组按照序列化数据的开始顺序排列成一个“符号表”。如果一个值中没有内联字符串或数据块,则可以省略此头部(并且由参考实现省略)。

注意,如果一个字符串和一个数据块具有完全相同的数据有效载荷,它们将唯一地放置在符号表的同一个槽位中。此外,注意空字符串和数据块永远不会内联,并且在值主体中分别用特殊的内联“空字符串”和“空数据块”标记表示。

此外,每个符号表条目声明相应的数据有效载荷是否可以(并将)用作字符串,或者只能作为数据块。这有助于反序列化器避免尝试将任意二进制数据解析为UTF-8。

如果一个符号表条目被多次引用,则会存储引用次数,所谓的“使用计数”与有效载荷一起。这使得反序列化器在符号的最后一次引用时能够将拥有的缓冲区交给消费者,从而节省了一次复制和分配。(这种优化仅在反序列化器与拥有缓冲区一起工作时才有意义。)

在符号表之后,所有其他数据都存储在值主体中。它包含原子值(例如布尔值、数字等)以及内联的数组映射。字符串和数据块通过它们在符号表中的索引进行引用。

在主体中存在多个值的唯一情况是当存在一个复杂类型(数组或映射),递归包含其他值时。数组中的值依次跟随。映射的键值交错:第一个条目是第一个键,然后是第一个值,接着是第二个键和第二个值,依此类推。

头部和主体中的类型标记

在头部/符号表中找到的类型标记的数值可能因它们是在头部/符号表中还是在实际值主体中而具有不同的解释。

以下列出了所有可能的主版本和次版本以及值标记。如前所述,NN 表示日志长度位,VVV VV 表示内联小整数位。

首先,头部和主体之间共享的类型标记

含义
000 000 NN 符号表开始

其次,头部/符号表中的类型标记

含义
010 VVV VV 短数据块,单次使用
011 VVV VV 短数据块,多次使用
100 VVV VV 短字符串,单次使用
101 VVV VV 短字符串,多次使用
111 010 NN 长数据块,单次使用
111 011 NN 长数据块,多次使用
111 100 NN 长字符串,单次使用
111 101 NN 长字符串,多次使用

第三,值主体中的类型标记

含义
000 001 00 null
000 001 01 可选存在
000 001 10 false
000 001 11 true
000 010 00 空字符串(内联)
000 010 01 空数据块(内联)
001 VVV VV 小有符号整数
010 VVV VV 小无符号整数
011 VVV VV 低索引字符串
100 VVV VV 低索引数据块
101 VVV VV 小数组
110 VVV VV 小映射
111 001 NN 有符号整数
111 010 NN 无符号整数
111 011 NN 字符串
111 100 NN 数据块
111 101 NN 数组
111 110 NN 映射
111 111 NN 浮点数

二进制格式的位级细节

首先,关于可变宽度数字编码的一些一般性说明:

  • 大小、计数、有效载荷长度和索引始终使用小端字节序的无符号整数进行编码。
  • 一些类型标记使用与适当的无符号整数相同的可变宽度编码结构来编码关联的长度/索引数据,只是它们可能使用不同的主/次类型标记。
  • 这种统一的可变宽度整数编码如下。
    • 如果类型标记的主类型表示“小”值,则5个最低有效位(#0-#4)包含整数值内联,无论是无符号还是2的补码有符号位序列。

      因此,内联小整数的范围

      • -16...+15(有符号)
      • 0...31(无符号)
    • 如果类型标记的主类型表示“大”值,则次类型标记确定值的实际(子)类型,并且两个最低有效位使用上述对数长度编码来引用下一个整数或浮点数的大小。因此,最低两位可能是以下之一00011011,分别表示随后的数字大小为1、2、4或8字节。2、4和8字节的数字存储为小端。

    • 因此,编码后的数字可能占用1、2、3、5或9个字节,具体取决于其大小。

接下来,详细说明符号表的格式。

符号表头部格式

如果存在符号表,值必须以主类型为“简单”和次类型为“symtab”的标记字节开始,即000 000 NN。如果值不是以这样的字节开始,则不读取符号表。

最后两位,NN,定义了编码符号表条目数所需字节的二进制对数。也就是说

符号表开始字节 整数编码符号计数宽度
000 000 00 1
000 000 01 2
000 000 10 4
000 000 11 8

注意,这意味着符号表计数没有紧凑的内联(1字节)格式来存储计数;总大小始终为2、3、5或9字节,因为“开始字节”无条件存在。

因此,符号计数随后作为小端无符号整数。例如,具有512个条目的符号表可能如下开始

+---------------+-------------+-------------+
|  000 000 01   |  0000 0000  |  0000 0010  |
+---------------+-------------+-------------+
| Symtab start, |  count,     |  count,     |
| 2^1 = 2 bytes |  low byte   |  high byte  |
| of length     |             |             |
| follow        |             |             |
+---------------+-------------+-------------+

然后,每个条目按顺序跟随,条目之间没有额外的填充。

符号表条目格式

符号表条目按以下顺序组成

  1. 类型标记和长度(1...9字节),通常使用可变宽度编码;
  2. 可选的使用计数(1...9字节),作为无符号整数;
  3. 原始有效载荷数据。

因此,前1...9个字节包含以下信息

  • 实际的符号类型,即
    • stringblob
    • 小(在类型标记中内联长度编码)或大
    • 单次使用或多次使用(是否在长度之后跟随使用计数)
  • 有效载荷长度,以字节为单位,作为无符号整数。

每个类型标记的位模式可以在上一节中所有类型标记的表中找到。

  • 如果类型标记的主类型表示多次使用符号,则使用计数作为通常的可变宽度编码中的无符号整数跟随。
  • 然而,如果主类型对应于单次使用符号,则不编码使用计数,解码器应假定其为1。

例如,一个长度为64字节的字符串,在值体中只被引用一次(因此具有隐式使用次数),可能被编码如下:

+--------------+-----------+------------------------+
|  111 100 00  | 0100 0000 | 64 bytes of UTF-8 data |
+--------------+-----------+------------------------+
| long string, | length,   | actual string payload  |
| single-use,  | 64 bytes  |                        |
| 2^0 = 1 byte |           |                        |
| of length    |           |                        |
| follow       |           |                        |
+--------------+-----------+------------------------+

同时,一个长度为17字节的blob在体中被使用了130次,可以这样编码:

+--------------+------------+-----------+---------------------+
|  011 100 01  | 111 010 00 | 1000 0010 | 17 arbitrary bytes  |
+--------------+------------+-----------+---------------------+
| short blob,  | use count: |    130    | actual blob payload |
| multi-use,   | unsigned   |           |                     |
| 17 bytes     | integer,   |           |                     |
| long         | 2^0 = 1    |           |                     |
|              | byte wide  |           |                     |
+--------------+------------+-----------+---------------------+

值体

非内部数据,即除了字符串和blob之外的所有类型的值,直接存储在体中,紧随头部之后。

每种类型值的精确格式如下所示。

类型标签 后续附加数据
000 001 00 null 无(仅标签)
000 001 01 可选 包装值
000 001 10 false 无(仅标签)
000 001 11 true 无(仅标签)
000 010 00 空字符串 无(仅标签)
000 010 01 空blob 无(仅标签)
001 VVV VV 小整数 无(5位值内联)
010 VVV VV 小无符号整数 无(5位值内联)
011 NNN NN 小字符串 无(5位符号表索引内联)
100 NNN NN 小blob 无(5位符号表索引内联)
101 NNN NN 小数组 数组元素(5位计数内联)
110 NNN NN 小映射 键值对(5位计数内联)
111 001 NN 大整数 2的NN次方字节的两进制补码
111 010 NN 大无符号整数 2的NN次方字节的未签数字
111 011 NN 大字符串 2的NN次方字节的符号表索引
111 100 NN 大blob 2的NN次方字节的符号表索引
111 101 NN 大数组 2的NN次方字节的计数,然后是元素
111 110 NN 大映射 2的NN次方字节的计数,然后是对
111 111 NN 浮点数 2的NN次方(4或8)字节的IEEE-754

示例

考虑Neodyn交易所的可读格式中MsgPack首页示例

{"compact": true, "schema": 0}

在二进制格式中,这表示为

地址 字节 说明
00 000 000 00 符号表开始,2的00次方 = 1字节长度
01 000 000 10 符号表中有2个符号
02 100 001 11 短字符串,单次使用,7字节
03 011 000 11 'c'
04 011 011 11 'o'
05 011 011 01 'm'
06 011 100 00 'p'
07 011 000 01 'a'
08 011 000 11 'c'
09 011 101 00 't'
10 100 001 10 短字符串,单次使用,6字节
11 011 100 11 's'
12 011 000 11 'c'
13 011 010 00 'h'
14 011 001 01 'e'
15 011 011 01 'm'
16 011 000 01 'a'
17 110 000 10 2元素映射
18 011 000 00 字符串 #0
19 000 001 11 true
20 011 000 01 字符串 #1
21 010 000 00 无符号整数 0

依赖关系

~3.5–6MB
~122K SLoC