GVariant 规范 1.0¶
1. 类型¶
根据要求,GVariant 必须与 DBus 消息总线系统基本兼容(如 [DBus] 中所述)。
为此,GVariant 中使用的类型系统几乎与 DBus 中使用的类型系统相同。然而,为了提供一个更好的系统,同时仍然保持高度兼容性,进行了一些非常小的更改;具体来说,可以发送到 DBus 的任何消息都可以表示为 GVariant。
从 DBus 继承了一些包袱,如果类型系统是从头开始设计的,这些包袱原本就不会存在。例如,对象路径和签名类型高度依赖于 DBus,如果从头开始创建通用类型系统,则不会存在这些类型。
1.1. 与 DBus 的差异¶
为了提高概念清晰度,一些限制已被解除,允许调用“永不失败”,而不必检查这些特殊情况。
虽然 DBus 限制了容器类型嵌套的最大深度,但 GVariant 不施加此类限制;嵌套支持到任意深度。
虽然 DBus 通过对“签名字符串”施加限制,使其长度不超过 255 个字符,来限制消息的复杂性,但 GVariant 不施加此类限制;支持任意长度的类型字符串,从而允许创建具有任意复杂类型的数值。
虽然 DBus 允许字典条目类型仅作为数组类型的元素类型出现,但 GVariant 不施加此类限制;字典条目类型可以独立存在,也可以作为任何其他类型构造器的子项。
虽然 DBus 要求结构类型包含至少一个子类型,但 GVariant 不施加此类限制;单元类型是 GVariant 中一个完全有效的类型。
DBus 的一些限制是作为安全考虑因素施加的(例如,为了限制可能由处理来自不受信任的发送者的消息而导致的递归深度)。如果 GVariant 在对这些考虑因素敏感的方式中使用,则程序员应在程序中输入来自不受信任来源的数值时,对这些情况进行检查。
此外,DBus 没有类型构造器来表达空值的概念 [1]。为此,添加了 Maybe 类型构造器(在类型字符串中用 m 表示)。
其中一些更改正在考虑纳入 DBus [2]。
1.2. 类型的枚举¶
1.2.1. 基本类型¶
- 布尔值
布尔值是一个必须为 True 或 False 的值。
- 字节
字节是一个值,通常是无符号的,范围从 0 到 255。
- 整数类型
除了字节之外,还有 6 种整数类型——有符号和无符号的 16、32 和 64 位整数。有符号版本具有与二进制补码表示法一致的数值范围。
- 双精度浮点数
双精度浮点数值由 IEEE 754 精确定义。
- 字符串
字符串是零个或多个字节。从官方角度来看,GVariant 与编码无关,但鼓励使用 UTF-8。
- 对象路径
DBus 对象路径,如 DBus 规范中所述。
- 签名
DBus 签名字符串,如 DBus 规范中所述。由于此类型仅为了与 DBus 兼容而保留,因此 DBus 对此类型的数值范围施加的所有限制均适用(例如,嵌套深度和最大长度限制)。
1.2.2. 容器类型¶
- 变体
变体类型是类型(本章中描述的任何类型,包括变体类型本身)和该类型的数值的依赖对。您可以使用此类型来克服数组的所有元素必须具有相同类型的限制。
- Maybe
maybe 类型构造器为任何其他单个类型提供空值。非空值是可区分的,因此,如果将多个 maybe 类型构造器应用于一个类型,则可以检测到不同级别的空值。
- 数组
数组类型构造器允许创建对应于提供的元素类型的数组(或列表)类型。必须提供一个元素类型,并且数组类型中的任何实例中的所有数组元素都必须具有此元素类型。
- 结构体
结构体类型构造器允许创建对应于提供的元素类型的结构体类型。这些“结构体”实际上更接近于元组,因为它们的字段没有命名,但使用“结构体”是因为 DBus 规范称它们为“结构体”。
结构体类型构造器是可变参数的类型构造器——可以提供任意数量的类型(包括零个和一个)。
- 字典条目
字典条目类型构造器允许创建一种特殊的结构体,当用作数组的元素类型时,这意味着数组的内容是键/值对的列表。为了与 DBus 兼容,此二进制类型构造器需要将其第一个参数作为基本类型(通常被视为键),但任何类型都可以作为第二个参数(通常是值)。
字典条目仅是约定俗成的;这包括当它们被放入数组中以形成“字典”时。GVariant 不施加通常期望于字典的限制(例如,键的唯一性)。
1.3. 类型字符串¶
就像 DBus 一样,使用简洁的字符串表示来表达类型。
在 GVariant 中,直接处理值作为一阶对象,类型字符串(也称为类型字符串)是表示单个类型的字符串。
与适用于消息的“签名字符串” [3] 形成对比,签名字符串包含零个或多个类型(对应于消息的参数)。
与 lseek() 系统调用的 whence 参数进行比较。
1.3.1. 语法¶
类型字符串的语言是上下文无关的。它也是一个前缀代码,这是语言本身递归结构使用的属性。
类型字符串可以用一个明确的上下文无关文法来描述。
type ⇒ base_type | container_type
base_type ⇒ b | y | n | q | i | u | x | t | s | o | g
container_type ⇒ v | m type | a type | ( types ) | { base_type type }
types ⇒ ε | type types
1.3.2. 语义¶
用于从给定文法获得类型字符串的推导会创建一个描述类型的抽象语法树。通过包含终端的每个右侧项进行推导的效果如下指定
- b
此推导对应于布尔类型。
- y
此推导对应于字节类型。
- n
此推导对应于有符号 16 位整数类型。
- q
此推导对应于无符号 16 位整数类型。
- i
此推导对应于有符号 32 位整数类型。
- u
此推导对应于无符号 32 位整数类型。
- x
此推导对应于有符号 64 位整数类型。
- t
此推导对应于无符号 64 位整数类型。
- d
此推导对应于双精度浮点数类型。
- s
此推导对应于字符串类型。
- o
此推导对应于对象路径类型。
- g
此推导对应于签名类型。
- v
此推导对应于变体类型。
- m type
此推导对应于 maybe 类型,该类型具有
Nothing或Just x的值,其中 x 在 type 的范围内。- a type
此推导对应于数组类型,其中每个元素具有类型 type。
- ( types )
此推导对应于结构体类型,该结构体具有由 types 扩展的类型,按顺序作为其项类型。
- { base_type type }
此推导对应于字典条目类型,该类型具有 base_type 作为其键类型和 type 作为其值类型。
2. 序列化格式¶
本章描述了 GVariant 使用的序列化格式。此序列化格式是新开发的,并且在此首次描述。
2.1. 为什么不使用 DBus?¶
由于 GVariant 在很大程度上与 DBus 兼容,因此使用 DBus 的序列化格式(并根据需要进行修改)作为 GVariant 的序列化格式似乎是合理的。
然而,这样做会与为 GVariant 建立的一些要求相冲突。
最根本的是,将违反要求。DBus 消息的编码方式是,为了获取数组中的第 100 个项目,您首先必须遍历前 99 个项目以发现第 100 个项目的位置。这种迭代的副作用将违反要求。
此外,使用 DBus 序列化格式和要求强制的 API 可能会导致违反要求,因为 DBus 消息的子部分在受到不同的起始对齐方式时,其含义可能会发生变化。这在 简单包含 中更详细地讨论。
2.2. 表示法¶
在本节中,将提供一些示例,使用类型和值的通用表示法。
用于类型的表示法与 类型 中描述的类型字符串完全相同。
用于值的表示法将熟悉 Python 或 Haskell 的用户。数组(列表)用方括号表示,结构体(元组)用括号表示。逗号分隔元素。字符串用单引号括起来。以 0x 为前缀的数字被视为十六进制数。
常量 True 和 False 表示布尔常量。maybe 类型的空构造器表示为 Nothing,单构造器表示为 Just。
2.3. 概念¶
GVariant 值序列化是值到字节序列和类型字符串对的总和和单射函数。序列化是确定性的,也就是说,从给定值序列化结果只有一个可接受的“规范形式”。序列化是非满射的:存在非规范形式。
序列化产生的字节序列如果没有类型字符串也是无用的。换句话说,反序列化字节序列需要知道该类型。
在讨论序列化的具体细节之前,有一些在格式设计中普遍存在的概念应该理解。
2.3.1. 字节序列¶
字节序列定义为具有已知长度的字节序列。在所有情况下,在 GVariant 中,知道长度对于成功反序列化值至关重要。
2.3.2. 字节边界¶
GVariant 中使用的起始和结束偏移量是指字节位置,而是字节边界。由于对于长度为 n 的字符串,可以有 n + 1 个前缀,因此在大小为 n 的字节序列中,有 n + 1 个字节边界。
字节边界¶
当谈论字节序列的起始位置时,起始边界的索引恰好对应于第一个字节的索引。但是,当谈论结束位置时,结束边界的索引将是最后一个字节的索引加 1。这种范例非常常用,并且允许指定零长度的字节序列。
2.3.3. 简单包含¶
存在多种能够拥有子值的容器类型。在所有情况下,容器的每个子值的序列化字节序列都将作为该容器的序列化字节序列的连续子序列出现——完全以其自身存在时的形式出现。子字节序列将按照它们在容器中的位置顺序出现。
容器有责任能够确定每个子元素的开始和结束位置(或者等效地,长度)。
此属性允许将容器分解为子值,只需将容器的字节序列的子序列作为子值的引用,这是一种有效的方式来满足需求。
对于DBus序列化格式,此属性不成立。在许多情况下(例如,数组),DBus消息中子值的编码会根据该值出现的上下文而变化。例如:对于双精度浮点数数组,如果紧接数组之前的数值结束位置是8的偶数倍,则数组将包含4个填充字节,而在前一个值的结束位置向前或向后偏移4个字节的情况下,则不会包含这些填充字节。
2.3.4. 对齐¶
为了满足需求,我们必须向程序员提供一个他们可以舒适使用的指针。在许多机器上,程序员无法直接引用未对齐的值,即使在可以引用的机器上,通常也会有性能损失。
因此,序列化格式中的所有类型都与其关联的对齐方式。对于字符串或单个字节,此对齐方式为1,但对于32位整数(例如),对齐方式为4。对齐方式是类型的属性——类型的任何实例都具有相同的对齐方式。
所有对齐的值都必须在内存中从其对齐方式的整数倍的地址开始。
容器类型的对齐方式等于该容器的任何潜在子类型的最大对齐方式。这意味着,即使32位整数数组为空,它仍然必须对齐到4字节的最近倍数。这也意味着变体类型(如下所述)具有8的对齐方式(因为它可能包含任何其他类型的数值,并且最大对齐方式为8)。
2.3.5. 固定大小¶
为了避免大量的帧开销,可以利用某些类型的所有实例都具有相同大小的事实。在这种情况下,该类型被称为固定大小的类型,并且它的所有值都被称为固定大小的值。示例包括单个整数以及整数和浮点数元组。反例包括字符串和整数数组。
如果类型具有固定大小,则此固定大小必须是该类型对齐方式的整数倍。类型永远不会具有零的固定大小。
如果容器类型始终持有固定数量的固定大小的项目(如某些结构或字典条目),则此容器类型也将是固定大小的。
2.3.6. 帧偏移量¶
如果容器包含非固定大小的子元素,则容器有责任能够确定它们的大小。这是使用帧偏移量完成的。
帧偏移量是某个预定大小的整数。该大小始终是2的幂。该大小由容器字节序列的总体大小确定。它被选择为刚好足够引用容器中的每个字节边界。
例如,大小为0的容器将具有大小为0的帧偏移量(因为不需要位来表示没有选择)。大小为1到255的容器将具有大小为1的帧偏移量(因为可以使用单个字节表示256个选择)。大小为256到65535的容器将具有大小为2的帧偏移量。大小为65536的容器将具有大小为4的帧偏移量。
帧偏移量的大小没有理论上限。这个事实(以及序列化格式中缺乏其他限制)允许任意大小的值。
在序列化时,必须通过“试验和错误”来确定正确的帧偏移量大小——检查每个大小以确定它是否有效。由于偏移量的大小包含在容器的大小中,因此较大的偏移量可能会将容器的大小提升到下一个类别,这反过来可能需要更大的偏移量。但是,这样的容器不会被认为是“规范形式”。如果序列化数据要处于规范形式,则必须使用尽可能小的偏移量大小。
帧偏移量始终出现在容器的末尾,并且未对齐。它们始终以小端字节顺序存储。
2.3.7. 字节序¶
虽然序列化数据的帧偏移量始终以小端字节顺序存储,但通过需求强制的接口可见的用户数据可以采用大端或小端字节顺序。这被称为“编码字节顺序”。在传输消息时,如果未明确约定,应指定此字节顺序。
编码字节顺序仅影响7种类型的值的表示:6种(16、32和64位,有符号和无符号)整数类型以及双精度浮点类型。在不同的编码字节顺序之间进行转换是一项简单的操作,通常可以就地执行(但请参阅关于字节交换的说明以获取例外情况)。
2.4. 基本类型的序列化¶
基本类型处理如下
2.4.1. 布尔值¶
布尔值具有固定大小1和对齐方式1。对于True,其值为1;对于False,其值为0。
2.4.2. 字节¶
字节具有固定大小1和对齐方式1。它可以具有任何有效的字节值。按照惯例,字节是无符号的。
2.4.3. 整数¶
有16、32和64位有符号和无符号整数。每种整数类型都具有固定大小(为其自然大小)。每种整数类型的对齐方式等于其固定大小。整数以编码字节顺序存储。有符号整数以二进制补码表示。
2.4.4. 双精度浮点数¶
双精度浮点数具有对齐方式和固定大小8。双精度数以编码字节顺序存储。
2.4.5. 字符串¶
包括对象路径和签名字符串,字符串不是固定大小的,并且对齐方式为1。任何给定序列化字符串的大小等于字符串的长度,加1,并且序列化的最后一个字节是空终止符(0)。字符串的字符集编码未指定,但不允许在字符串的内容中出现空字节。
2.5. 容器类型的序列化¶
容器处理如下
2.5.1. 变体¶
变体通过存储子序列化数据,加上零字节,加上子类型的类型字符串进行序列化。
需要零字节,因为虽然类型字符串是前缀代码,但它们不是后缀代码。如果没有此分隔符,请考虑将变体序列化为两个字节——“ay”。这是单个字节“a”,还是字节的空数组?
2.5.2. Maybe¶
Maybe的编码方式取决于其元素类型是固定大小的还是不是。
Maybe类型的对齐方式始终等于其元素类型的对齐方式。
2.5.2.1. 固定大小元素的Maybe¶
对于Nothing情况,序列化数据是空字节序列。
对于Just情况,序列化数据与子序列化数据完全相同。这始终可以与Nothing情况区分开,因为所有固定大小的值都具有非零大小。
2.5.2.2. 非固定大小元素的Maybe¶
对于Nothing情况,序列化数据再次是空字节序列。
对于Just情况,序列化形式是子元素的序列化数据,后跟单个零字节。此额外的字节可确保即使子值的大小为零,也可以区分Just情况与Nothing情况。
2.5.3. 数组¶
根据其元素类型是固定大小的类型还是不是,数组被认为是固定宽度数组或可变宽度数组。这两种情况的编码非常不同。
数组类型的对齐方式始终等于其元素类型的对齐方式。
2.5.3.1. 固定宽度数组¶
在这种情况下,每个数组元素的序列化形式按顺序打包,没有额外的填充或帧,以获得数组。由于所有固定大小的值都具有其对齐方式要求的倍数的大小,并且数组中的所有元素都将具有相同的对齐方式要求,因此所有元素都自动对齐。
16位整数数组¶
可以通过获取数组的大小并将其除以固定元素大小来确定数组的长度。这始终有效,因为所有固定大小的值都具有非零大小。
2.5.3.2. 可变宽度数组¶
在这种情况下,每个数组元素的序列化形式再次按顺序打包。与固定宽度的情况不同,可能需要在元素之间添加填充字节以进行对齐。这些填充字节必须为零。
在添加所有元素之后,将为每个元素追加一个帧偏移量,按顺序排列。帧偏移量指定该元素的结束边界。
字符串数组¶
每个帧偏移量的大小是数组序列化大小和最终帧偏移量的函数,通过识别数组中最后一个元素的结束边界也识别了帧偏移量的起始边界。由于数组中的每个元素都有一个帧偏移量,因此我们可以轻松确定数组的长度。
要找到任何元素的起始位置,只需获取前一个元素的结束边界并将其向上舍入到数组(因此元素)对齐方式的最近整数倍。第一个元素的起始位置是数组的起始位置。
由于确定数组的长度依赖于我们计算帧偏移量的数量,并且帧偏移量的数量由它们所占用的空间确定,因此即使在所有其他序列化数据的大小为零的情况下,也不允许在数组中使用零字节帧偏移量。此特殊例外情况避免了将零除以零并想知道答案的情况。
2.5.4. 结构体¶
与数组一样,结构体通过按顺序存储每个子项,正确对齐并带有零填充字节进行序列化。
在添加所有项目之后,将按相反的顺序为每个非固定大小的项(不是结构体中的最后一个项)追加一个帧偏移量。帧偏移量指定该元素的结束边界。
帧偏移量按相反的顺序存储,以便基于迭代器的接口可以从开始迭代结构体中的项目,而无需首先测量由类型字符串暗示的项目数量(这是一种时间复杂度为字符串大小的线性运算)。
包含16位整数和字符串的结构体¶
不为结构体中的最后一个项目存储帧偏移量,因为其结束边界可以通过从结构体的大小中减去帧偏移量的大小来确定。结构体给定类型的任何实例中存在的帧偏移量数量可以完全从类型(按照上述规则)确定。
不为固定大小的项目存储帧偏移量,因为它们的结束边界始终可以通过将固定大小添加到起始边界来找到。
要查找结构中任何项目的起始边界,只需从最近的非固定大小项目(或如果没有前面的非固定大小项目则从 0)的结束边界开始。从那里,向上取整以进行对齐,并为每个中间项目添加固定大小。最后,向上取整到所需项目的对齐方式。
对于随机访问,似乎此过程可能需要与结构中元素数量成线性的时间,但实际上可以在很小的常数时间内执行。请参阅 计算结构项目地址。
如果结构中包含的所有项目都是固定大小的,那么该结构本身也是固定大小的。必须考虑满足施加在此固定大小值上的约束条件。
首先,固定大小必须是非零的。这种情况只会发生在单元类型结构或仅包含此类结构(递归地)的结构中。通过任意声明单元类型的实例的序列化编码为单个零字节(大小为 1)来解决此问题。
其次,固定大小必须是结构对齐方式的倍数。通过在任何固定宽度结构的末尾添加零填充字节来实现,直到此属性为真。这些字节绝不会导致对定位框架偏移量或变量大小子项的结尾产生混淆,因为根据定义,这些事情都不会发生在固定大小的结构内部。
图 2.4 描述了一个类型为 (nsns) 且值为 [257, 'xx', 514, ''] 的结构。对于一个非固定大小且不是最后一个项目的项目(即字符串 'xx'),存在一个框架偏移量。指示了“向上取整”以找到第二个整数的起始位置的过程。
2.5.5. 字典条目¶
字典条目被视为具有恰好两个项目的结构——首先是键,然后是值。如果键是固定大小的,则不会有框架偏移量;如果键是非固定大小的,则将恰好有一个。由于值被视为结构中的最后一个项目,因此它永远不会有框架偏移量。
2.6. 示例¶
本节包含一些说明性示例,以演示序列化格式。所有示例均采用小端字节顺序。
示例数据以每行 16 个字节的形式给出,其中两个字符表示每个字节的值。为了清晰起见,根据用途使用许多不同的字节值表示法。
'A表示一个字节具有 ASCII 值为A(65)。
sp表示一个字节是 ASCII 空格字符 (32)。
\0表示一个字节是零字节,用于标记字符串的结尾。
--表示该字节是零填充字节,用作结构或字典条目的一部分。
##表示该字节是零填充字节,用作数组的一部分。
@@表示该字节是Just值的末尾的零填充字节。任何两个十六进制数字表示一个字节具有该值。
每个示例都指定一个类型、一个字节序列以及在使用给定类型进行反序列化时此字节序列所代表的值。
- 字符串示例
类型为
's''h 'e 'l 'l 'o sp 'w 'o 'r 'l 'd \0
具有值为
'hello world'。- Maybe 字符串
类型为
'ms''h 'e 'l 'l 'o sp 'w 'o 'r 'l 'd \0 @@
具有值为
Just 'hello world'。- 布尔数组示例
类型为
'ab'01 00 00 01 01
具有值为
[True, False, False, True, True]。- 结构示例
类型为
'(si)''f 'o 'o \0 ff ff ff ff 04
具有值为
('foo', -1)。- 结构数组示例
类型为
'a(si)''h 'i \0 -- fe ff ff ff 03 ## ## ## 'b 'y 'e \0 ff ff ff ff 04 09
具有值为
[('hi', -2), ('bye', -1)]。- 字符串数组示例
类型为
'as''i \0 'c 'a 'n \0 'h 'a 's \0 's 't 'r 'i 'n 'g 's '? \0 02 06 0a 13
具有值为
['i', 'can', 'has', 'strings?']。- 嵌套结构示例
类型为
'((ys)as)''i 'c 'a 'n \0 'h 'a 's \0 's 't 'r 'i 'n 'g 's '? \0 04 05
具有值为
(('i', 'can'), ['has', 'strings?'])。- 简单结构示例
类型为
'(yy)'70 80
具有值为
(0x70, 0x80)。- 填充结构示例 1
类型为
'(iy)'60 00 00 00 70 -- -- --
具有值为
(96, 0x70)。- 填充结构示例 2
类型为
'(yi)'70 -- -- -- 60 00 00 00
具有值为
(0x70, 96)。- 结构数组示例
类型为
'a(iy)'60 00 00 00 70 -- -- -- 88 02 00 00 f7 -- -- --
具有值为
[(96, 0x70), (648, 0xf7)]。- 字节数组示例
类型为
'ay'04 05 06 07
具有值为
[0x04, 0x05, 0x06, 0x07]。- 整数数组示例
类型为
'ai'04 00 00 00 02 01 00 00
具有值为
[4, 258]。- 字典条目示例
类型为
'{si}''a sp 'k 'e 'y \0 -- -- 02 02 00 00 06
具有值为
{'a key', 514}。
2.7. 非规范序列化数据¶
从理论上讲,反序列化是序列化的逆操作。这意味着反序列化应该是一个双射偏函数。
如果反序列化是一个偏函数,则必须对序列化数据不在规范形式的情况做些处理。通常这将导致引发错误。
2.7.1. 反对错误的论点¶
需求 XXX 禁止我们在加载时扫描整个序列化字节序列;我们无法在此时间检查规范性并发出错误。这会将任何可能发生的错误留给在访问值时作为异常引发。
鉴于 C 语言对异常的支持较差(实际上几乎不存在),并且任何对简单数据值的访问都可能失败的想法,这种解决方案也迅速变得不可行。
在我们的约束条件下,处理错误的唯一合理解决方案是将其定义为不存在。接受非规范形式的序列化数据使反序列化成为一个满射(但不内射)完全函数。所有字节序列都反序列化为一些有效值。
出于安全目的,对非规范值所做的事情被精确指定。可以很容易地想象一种情况,内容过滤器正在作用于消息的内容,从而调节对安全敏感组件的访问。如果可以创建消息的非规范形式,该形式由过滤器中的反序列化器和安全敏感组件中的反序列化器解释不同,就可以“绕过”过滤器。
2.7.2. 默认值¶
当在反序列化过程中遇到错误时,由于无法引发异常,我们被迫进入一种必须返回预期类型有效值的情况。出于这个原因,为每种类型定义了一个“默认值”。此值通常是反序列化过程中遇到的错误的结果。
有人可能会认为,忽略错误并向用户返回任意值会导致鲁棒性降低。然而,应该指出的是,对于大多数类型的序列化数据,随机字节错误更有可能使数据保持规范形式,但具有不同的值。我们无法捕获这些情况,并且这些情况可能会导致返回给用户的给定类型的任何可能值。我们被迫接受这样一种事实,即在存在损坏的情况下,我们能做的最好的事情是确保用户收到正确类型的某个值。
每种类型的默认值是
- 布尔值
默认布尔值为 False。
- 字节
默认字节值为 null。
- 整数
任何大小的整数(有符号或无符号)的默认值为零。
- 浮点数
双精度浮点数的默认值为正零。
- 字符串
字符串的默认值为空字符串。
- 对象路径
对象路径的默认值为
'/'。- 签名
签名的默认值是空签名(即:空字符串)。
- 数组
任何类型数组的默认值是该类型的空数组。
- Maybes
任何类型 maybe 的默认值是该类型的
Nothing。- 结构
结构类型的默认值是结构实例,该实例具有每个项目的默认值,该值是该项目类型的默认值。
- 字典条目
与结构类似,字典条目类型的默认值是字典条目实例,其键和值等于各自的默认值。
- 变体
变体的默认值为包含单元类型子项的变体。
2.7.3. 处理非规范序列化数据¶
在正常运行的系统上,通常不会遇到非规范值,因此一旦检测到问题,可以接受性能任意差。但是,出于安全原因,必须在访问时始终检查不受信任的数据是否符合规范性。由于这些检查的频率,它们必须很快。
本节包含的几乎所有规则都牢记了此要求。具体而言,所有规则都可以在很小的常数时间内确定(只有几个非常小的例外)。例如,要求扫描具有不一致框架偏移量的数组以确定数组大小是不允许的,因为这需要线性时间(与数组大小成线性关系)。
序列化字节序列中只有一小部分不同的异常情况可能发生。本节中介绍了每种情况以及如何处理它。
以下列表旨在成为一个明确的列表。如果序列化字节序列没有这些问题,则它就是规范形式。如果序列化字节序列具有任何这些问题,则它不是规范形式。
- 固定大小值的大小错误
如果用户尝试使用固定宽度类型的类型进行反序列化,并且字节序列的长度不正确,则将使用该类型的默认值。
- 非零填充字节
此异常发生在任何填充字节都是非零时。这适用于数组、maybes、结构和字典条目。此异常从不进行检查——子值从其容器中反序列化,就好像填充为零填充一样。
- 布尔值超出范围
如果布尔值包含 0 或 1 以外的数字,则将其视为 true。这是为了与用户直接在 C 中访问布尔数组保持一致。例如,如果数组中的一个字节包含数字 5,则在 C 中将评估为 True。
- 可能未终止的字符串
如果字符串的序列化形式的最后一个字节不是零字节,则字符串的值将采用空字符串。
- 字符串中嵌入了空字符
如果字符串在其最后一个字节处包含空字符,但在该最终终止符之前也包含另一个空字符,则字符串的值将采用位于嵌入空字符之前的字符串部分。这意味着获取 C 指向字符串的指针仍然是一个常数时间操作。
- 无效对象路径
如果对象路径的序列化形式不是有效的对象路径,后跟零字节,则使用默认值。
- 无效签名
如果签名字符串的序列化形式不是有效的 DBus 签名,后跟一个零字节,则使用默认值。
- 固定大小 Maybe 的尺寸错误
如果一个具有固定元素大小的 maybe 实例不完全等于该元素的大小,则该值被认为是
Nothing。- 固定宽度数组的尺寸错误
如果固定宽度数组的序列化大小不是固定元素大小的整数倍,则该值被认为是空数组。
- 子元素的起始或结束边界超出容器范围
如果框架偏移量(或基于它们的计算)指示子值字节序列的任何部分将超出父字节序列的范围,则子元素将被赋予其类型的默认值。
- 结束边界先于起始边界
如果框架偏移量(或基于它们的计算)指示子值字节序列的结束边界先于其起始边界,则子元素将被赋予其类型的默认值。
子元素的结束边界先于起始边界可能会导致两个或多个子元素的字节序列重叠。此错误对于其他子元素将被忽略。这些子元素将被赋予与对这些字节序列执行的正常反序列化过程以及子元素的类型相对应的数值。
如果容器中的子元素顺序不正确,则表示存在这种异常。不会对顺序不正确的子元素执行任何其他特定检查。
- 子值重叠框架偏移量
如果子值字节序列与其所在容器的框架偏移量重叠,则此错误将被忽略。子元素将被赋予与对该字节序列(包括框架偏移量中的字节)执行的正常反序列化过程以及子元素的类型相对应的数值。
- 非固定宽度数组的无意义长度
如果非固定宽度数组的最终框架偏移量指向超出数组字节序列的边界,或者指示数组中存在非整数个框架偏移量,则该值被认为是空数组。
- 结构框架偏移量空间不足
如果序列化的结构包含存储所需数量的框架偏移量的空间不足,则只要正在访问的项目具有其所需的框架偏移量,该错误将被静默忽略。尝试访问超出可用偏移量的项目将导致默认值。
2.7.4. 示例¶
本节包含一些说明性示例,以演示非正常数据的正确反序列化。
字节序列以与正常形式示例相同的形式呈现。简要描述了为什么值反序列化为给定的值。
- 固定大小值的大小错误
类型为
'i'07 33 90
的值为
0。由于任何类型为
'i'的值都应具有 4 个字节的序列化大小,并且由于仅提供了 3 个字节,因此使用默认值零代替。- 非零填充字节
类型为
'(yi)'55 66 77 88 02 01 00 00
的值为
(0x55, 258)。非零填充字节 (
66 77 88) 简单地被忽略。- 布尔值超出范围
类型为
'ab'01 00 03 04 00 01 ff 80 00
的值为
[True, False, True, True, False, True, True, True, False]。任何非零布尔值都被视为
True。- 未终止的字符串
类型为
'as''h 'e 'l 'l 'o sp 'w 'o 'r 'l 'd \0 0b 0c
的值为
['', ''](两个空字符串)。第二个字符串正常反序列化为单个空字符,但第一个字符串不包含空字符。无论紧随其后的是空字符,第一个字符串都将被替换为空字符串(字符串的默认值)。
- 字符串中嵌入了空字符
类型为
's''f 'o 'o \0 'b 'a 'r \0
的值为
'foo'。- 嵌入空字符但末尾没有空字符的字符串
类型为
's''f 'o 'o \0 'b 'a 'r
的值为
''(空字符串)。始终检查字符串中的最后一个字节以确定是否存在空字符,如果不存在,则使用空字符串作为该值。这包括字符串中存在空字符的情况。
- 固定大小 maybe 的尺寸错误
类型为
'mi'33 44 55 66 77 88
的值为
Nothing。类型为
'mi'的值只有在序列化形式完全为 4 个字节时才可能为Just。- 固定宽度数组的尺寸错误
类型为
'a(yy)'03 04 05 06 07
的值为
[]。由于每个数组元素都是一对字节,因此数组的序列化大小应该是二的倍数。由于不是这种情况,因此数组的值是空数组。
- 子元素的起始或结束边界超出容器范围
类型为
'(as)''f 'o 'o \0 'b 'a 'r \0 'b 'a 'z \0 04 10 0c
的值为
['foo', '', '']。在解包数组中的第一个元素时没有遇到问题(标记为位于字节边界 0 和 4 之间)。在解包第二个元素时,其结束偏移量 (16) 超出了数组的范围。此偏移量 (16) 也是第三个数组元素的起始位置。因此,这两个元素都被赋予其默认值(空字符串)。
- 结束边界先于起始边界
类型为
'(as)''f 'o 'o \0 'b 'a 'r \0 'b 'a 'z \0 04 00 0c
的值为
['foo', '', 'foo']。再次,在解包数组中的第一个元素时没有遇到问题。在解包第二个元素时,注意到结束边界先于起始边界。由于这是不可能的,因此使用默认值
''代替。解包最后一个元素(从 0 到 12)没有问题。最后一个元素与第一个元素重叠,但是,并且在评估其值时,嵌入的空字符导致它在'foo'处被截断。- 结构框架偏移量空间不足
类型为
'(ayayayayay)'03 02 01
的值为
([3], [2], [1], [], [])。由于这不是固定大小的值,因此其大小不可能导致它接收其默认值(即:没有“最小尺寸”的概念)。解包结构中的前三个项目没有问题(表明一个值的内容可以与框架偏移量重叠)。但是,尝试解包最后两个项目失败了,因为所需的框架偏移量根本不存在。使用默认值代替。
3. 实现格式¶
本章包含有关序列化格式的信息,这些信息不属于其规范。
此信息讨论了在实施序列化格式期间会出现的问题。当然,本章讨论的问题对第章中讨论的 GVariant 实现产生了影响。
对字节交换操作的安全性提出了一些不幸的观察,并提供了一种方法(以及正确性证明),即使对于固定大小的值省略了框架偏移量,也可以以恒定时间对结构的內容进行随机访问。
3.1. 关于字节交换的说明¶
实施者可能希望对序列化的 GVariant 数据执行就地字节交换。在这种情况下,需要考虑一些事项。
主要问题源于存在非正常序列化数据的事实,在这种情况下,字节交换可能无法实现。
对于类型字符串为 (ssn),请考虑以下在小端字节顺序中的非正常序列化数据
78 00 00 02
第一个字符串的长度为 2(包括空终止符),值为 'x'。由于其结束偏移量 0 先于其起始偏移量 2,因此第二个字符串被赋予其默认值 ''。最后,16 位整数,其起始偏移量为 0(因此与第一个字符串重叠),其值为 0x78。整个结构的价值为 ('x', '', 120)。
要将此序列化数据更改为大端字节顺序,需要交换 16 位值的字节。但是,这样做也会修改这些字节重叠的字符串的值。在这种情况下(通常情况下),无法避免此问题。
由于此问题,任何希望对序列化数据执行就地字节交换的实现都必须首先确保数据处于正常形式。
在某些情况下,不需要这种正常形式。对于任何固定大小的值或可变大小的数组,不存在框架偏移量。这有效地消除了重叠数据的可能性,这意味着这些情况可以在不首先检查正常性的情况下就地进行字节交换。
由于环境的幸运对齐,这些类型(以及字符串,它们根本不需要字节交换)正是实施者可能希望通过指针向用户公开的数据类型。因此,很容易想象实施者最终可能不需要在始终安全的情况下对序列化数据进行就地字节交换的能力。
3.2. 计算结构项目地址¶
在 C 语言中,结构与它们在序列化格式中的存在方式非常相似。结构的每个项目都尽可能紧密地跟随其前面的项目,受对齐约束的影响。
无论做什么,都无法以恒定时间确定 C 中结构中项目的地址。需要考虑其前面项目的尺寸和对齐方式——这个过程需要的时间不能少于线性时间。执行此算法是从结构的起始地址开始,然后对于结构中的每个前面的项目,向上舍入到其对齐要求并添加其大小。最后,向上舍入到要访问的项目对齐要求。
可以使用包含两种操作类型的简单代数来描述此过程
\((+c)\):将某个常数 \(c\) 添加到自然数。
\((↑c)\):“对齐”(向上舍入)自然数到某个常数 2 的幂的最近倍数 \(2^c\)。
假设编译器将整数值对齐到其大小。例如,给定结构的起始地址 \(s\),要找到一个 32 位整数的地址,该整数紧随一个 16 位整数和三个 64 位整数的数组之后,必须执行以下计算:
当然,没有理智的 C 编译器会在每次访问时保存此计算。相反,编译器会在结构定义时执行计算并构建一个包含每个结构项目的起始偏移量和大小的表。由于结构中的每个项目的大小都是固定的,并且结构的起始地址始终正确对齐,因此结构的项目的地址始终可以表示为相对于该结构起始地址的常数。
对于我们的例子
允许非固定大小的项目进入结构显然会阻止任何非固定大小项目后面的项目的起始偏移量相对于结构的起始位置成为常数。任何项目的起始地址将明显取决于紧随其后的非固定大小项目的结束地址。更糟糕的是,由于此结束地址没有特定的对齐方式,因此即使相对于紧随其后的非固定大小项目的结束位置,每个项目的起始偏移量也无法表示为常数偏移量。
如果没有发现另一种构建表的方法,则必须执行完整的计算,即线性时间。幸运的是,存在另一种方法,允许以恒定时间访问结构成员。有可能构建一个表,其中每行包含四个整数,该表允许以仅四个操作计算任何结构项目的起始地址
其中 \(offsets\) 是结构的框架偏移量数组,\(i\)、\(a\)、\(b\) 和 \(c\) 是表中的四个整数。按定义,\(offsets[{-1}] = 0\)。
3.2.1. 执行简化¶
本质上,我们感兴趣的是一个可以将任何长度的常数加法和对齐操作序列简化为长度为 3 的序列的过程,形式如上所示。然后,我们可以在每次访问时执行这少量常数操作,而不是执行完整的计算。
此简化过程根据以下简化规则进行
- 加法规则
\((+a);\ (+b) ⇒ (+(a + b))\)
- 更大的对齐规则
\((↑a);\ (+b);\ (↑c) ⇒ (+(b ↑ a));\ (↑c)\), 其中 \(c ≥ a\)
- 较小对齐规则
\((↑a);\ (+b);\ (↑c) ⇒ (↑a);\ (+(b ↑ c))\), 其中 \(c ≤ a\)
我们可以证明,使用这些规则,任何操作序列都可以简化到最多只有一个对齐操作。如果序列中存在两个对齐操作,则以下情况之一必须为真
两个对齐操作之间恰好有一个加法
两个相邻的对齐操作
两个对齐操作之间有多个加法
如果两个对齐操作之间恰好有一个加法,则可以立即应用较大或较小对齐规则,从而将对齐操作的数量减少一个。
如果存在多个加法,则可以通过应用加法规则将其合并为一个加法,然后再应用其中一个对齐规则。在两个相邻对齐操作的情况下,可以在它们之间引入一个 \((+0)\) 操作,然后再应用其中一个对齐规则。
由于我们可以将任何操作序列简化为仅包含一个对齐操作的序列,因此我们可以通过使用加法规则合并发生在此单个对齐操作之前和之后的所有加法,进一步将其简化为 \((+a);\ (↑b);\ (+c)\) 的形式。
3.2.2. 计算表格¶
基于上述简化规则,可以开发一种高效(但仍然是线性时间)的算法来一次性计算整个表格。
始终将“到目前为止的状态”保存在四个变量中:\(i\)、\(a\)、\(b\) 和 \(c\),使得到达当前位置可以通过相对于 \(offset[i]\) 计算 \(((+a);\ (↑b);\ (+c))\) 来实现。\(i\) 保持等于框架偏移量的索引,该索引指定结构中遇到的最新非固定大小项目的末尾(或者在没有遇到此类项目的情况下为 \(-1\))。\(a\)、\(b\)、\(c\) 从 0 开始。
定义了三个合并规则,允许将任何附加操作附加到此序列,而不会更改序列的形式的大小;合并规则仅影响 \(a\)、\(b\) 和 \(c\) 的整数值。
附加小于或等于当前对齐的对齐 \(d\):\((a, b, c) := (a, b, c ↑ d)\) 作为较小对齐规则应用 \((+a);\ (↑b);\ (+c);\ (↑d) ⇒ (+a);\ (↑b) (+c ↑ d)\) 的直接结果。
附加大于当前对齐的对齐 \(d\):\((a, b, c) := (a + (c ↑b), d, 0)\) 通过较大对齐规则应用 \((+a);\ (↑b);\ (+c);\ (↑d) ⇒ (+a);\ (+c ↑ b);\ (↑d)\),对 \((+a + (c ↑ b));\ (↑d)\) 应用加法规则,以及无害地附加 \((+0)\) 以得到 \((+a + (c ↑ b));\ (↑d);\ (+0)\)。
附加一个加法 \(e\):\((a, b, c) := (a, b, c + e)\) 通过显式使用加法规则 \((+a);\ (↑b);\ (+c);\ (+e) ⇒ (+a);\ (↑b);\ (+(c + e))\)。
每当遇到一个非固定大小的项目时,\(i\) 递增,并且 \(a\)、\(b\)、\(c\) 被重置为零。
该算法由以下 Python 函数实现,该函数将 (对齐、固定大小) 对列表作为输入,表示结构项。其输出是表格,以 4 元组数组的形式给出。
def generate_table (items):
(i, a, b, c) = (-1, 0, 0, 0)
table = []
for (d, e) in items:
if d <= b:
(a, b, c) = (a, b, align(c, d)) # merge rule #1
else:
(a, b, c) = (a + align(c, b), d, 0) # merge rule #2
table.append ((i, a, b, c))
if e == -1: # item is not fixed-sized
(i, a, b, c) = (i + 1, 0, 0, 0)
else:
(a, b, c) = (a, b, c + e) # merge rule #3
return table
假设 align(a, b) 计算 \((a ↑ b)\)。
3.2.3. 进一步简化¶
上述简化是非一致的。最终操作序列上存在等价关系。具体来说,如果 \(d\) 是 \(2^b\) 的倍数,则
这是因为,作为 \(2^b\) 的倍数,\(d\) 可以“穿过”对齐操作而不改变。
例如,考虑以下情况
显然,这等同于
因为 16 的二进制表示中没有低阶位受到仅清除最低 3 位的舍入操作的影响。
在遇到的小对齐约束(不超过 8)的情况下,可以通过将 256 的倍数从 \(c\) 移到 \(a\) 来确保 \(c\) 适合不超过一个字节。考虑到遇到的最大对齐约束为 3,这适用于指定的序列化格式。
3.2.4. 加/与/或表示¶
作为一项微优化,在执行上一节中的简化之后,\(a\)、\(b\)、\(c\) 的结果值可以转换为仅使用 3 个常用机器指令即可执行计算的状态。
这种转换利用了关于舍入的三个简单事实。
首先,向上舍入到最接近的数字的倍数与添加该数字减 1,然后向下舍入到最接近的该数字的倍数相同。
其次,向下舍入到某个数字的倍数(该数字是 2 的幂)与该数字的补码进行按位与运算相同。
第三,舍入到 2 的幂的倍数的结果导致结果的低阶位被清除。将小于该倍数的数字添加到舍入结果中不可能导致进位,因此使用按位或运算是等效的运算。
考虑到在上一节的简化之后,\(c < 2^b\)
其中 \(|\) 表示按位或,\(\&\) 表示按位与,\(\sim\) 表示按位补码。
因此,我们可以选择将以下内容存储到表中
并且对于每个地址,我们只需要执行加法、按位与和按位或运算。
3.2.5. 简化规则的证明¶
在给出一些“直观”引理的情况下,我们可以证明简化规则是合理的。
- 引理 1
- \[\forall{a, b}: (↑a);\ (↑b) = (↑(max(a, b)))\]
因为对齐始终是对 2 的幂,所以两个连续的对齐操作等同于这两个操作中“最强大”的一个。
- 引理 2
- \[\forall{a, b, c, r}: r = (↑c) ⇒ r(a) + r(b) = r(a + r(b))\]
因为 \(r(b)\) 已经是 \(2c\) 的倍数,它可以“穿过”第二次 \(r\) 的应用而不改变。
- 引理 3
- \[\forall{c}, (0 ↑ c) = 0\]
3.2.5.1. 加法规则
加法的结合律
这与以下内容相同
通过部分实例化
然后通过外延性
3.2.5.2. 较大对齐规则
设 \(r = (↑c)\) 和 \(s = (↑a)\)。
引理 2
引理 3 允许
将上述内容重复应用引理 2
这当然等同于
由于加法是可交换的,并且我们普遍量化 \(m\) 和 \(n\),因此对一个有效的工作不会对另一个产生影响
因此,显然
我们可以将其部分实例化为
那么,必须为真
记住 \(r = (↑c)\) 和 \(s = (↑a)\)
引理 1(因为 \(a ≤ c\))将其合并为
通过外延性
3.2.5.3. 较小对齐规则
设 \(r = (↑a)\) 和 \(s = (↑c)\)。
显然
从引理 1,因为 \(c ≤ a\)
然后引理 2 允许
有效地反转了引理 1 的第一次应用
记住 \(r = (↑a)\) 和 \(s = (↑c)\)
通过外延性