2. CBOR 编码规范
CBOR 编码的数据项按本节所述进行构造和编码. 该编码在表 5 中作了概述.
每个数据项的初始字节既包含关于主类型的信息 (高 3 位, 见第 2.1 节), 也包含附加信息 (低 5 位). 当附加信息的值小于 24 时, 它直接用作一个小的无符号整数. 当它为 24 到 27 时, 变长整数的附加字节紧随其后; 附加信息的取值 24 到 27 分别指明其长度为一个 1、2、4 或 8 字节的无符号整数. 附加信息值 31 用于不定长数据项, 见第 2.2 节. 附加信息值 28 到 30 保留供未来扩展.
在所有附加信息取值中, 所得的整数都根据主类型来解释. 它可能表示实际数据: 例如, 在整数类型中, 所得的整数就用作该值本身. 它也可能提供长度信息: 例如, 在字节串中, 它给出其后跟随的字节串数据的长度.
CBOR 解码器的实现可以基于一张跳转表, 其中包含初始字节全部 256 个已定义的取值 (表 5). 受限实现中的解码器则可以利用初始字节和后续字节的结构来获得更紧凑的代码 (关于这可能是什么样子的大致印象, 见附录 C).
2.1. 主类型
以下列出各主类型以及与该类型相关的附加信息和其他字节.
主类型 0: 无符号整数. 5 位附加信息要么是整数本身 (对于附加信息取值 0 到 23), 要么是附加数据的长度. 附加信息 24 表示该值由一个额外的 uint8_t 表示, 25 表示 uint16_t, 26 表示 uint32_t, 27 表示 uint64_t. 例如, 整数 10 表示为单字节 0b000_01010 (主类型 0, 附加信息 10). 整数 500 则为 0b000_11001 (主类型 0, 附加信息 25), 后跟两个字节 0x01f4, 即十进制的 500.
主类型 1: 负整数. 其编码遵循无符号整数 (主类型 0) 的规则, 只是该值随后为 -1 减去所编码的无符号整数. 例如, 整数 -500 为 0b001_11001 (主类型 1, 附加信息 25), 后跟两个字节 0x01f3, 即十进制的 499.
主类型 2: 字节串. 该字符串以字节计的长度按正整数 (主类型 0) 的规则表示. 例如, 长度为 5 的字节串其初始字节为 0b010_00101 (主类型 2, 附加信息 5 表示长度), 后跟 5 字节的二进制内容. 长度为 500 的字节串则有 3 个初始字节 0b010_11001 (主类型 2, 附加信息 25 表示两字节长度), 后跟表示长度 500 的两个字节 0x01f4, 再后跟 500 字节的二进制内容.
主类型 3: 文本串, 具体而言是编码为 UTF-8 [RFC3629] 的 Unicode 字符串. 该类型的格式与字节串 (主类型 2) 完全相同, 也就是说, 与主类型 2 一样, 长度给出的是字节数. 该类型用于需要解释或显示人类可读文本的系统, 并允许区分非结构化字节与具有指定字符集和编码的文本. 与 JSON 等格式相比, 该类型中的 Unicode 字符从不进行转义. 因此, 换行字符 (U+000A) 在字符串中始终表示为字节 0x0a, 而从不表示为字节 0x5c6e (即字符 "" 和 "n") 或 0x5c7530303061 (即字符 ""、"u"、"0"、"0"、"0" 和 "a").
主类型 4: 数据项数组. 数组也称为列表、序列或元组. 数组的长度遵循字节串 (主类型 2) 的规则, 只是长度表示的是数据项的个数, 而不是数组所占的字节长度. 数组中的项不必都属于同一类型. 例如, 包含 10 个任意类型项的数组其初始字节为 0b100_01010 (主类型 4, 附加信息 10 表示长度), 后跟其余 10 个项.
主类型 5: 数据项对的映射. 映射也称为表、字典、哈希或 (在 JSON 中) 对象. 映射由数据项对组成, 每一对由一个键紧随一个值构成. 映射的长度遵循字节串 (主类型 2) 的规则, 只是长度表示的是对的个数, 而不是映射所占的字节长度. 例如, 包含 9 对的映射其初始字节为 0b101_01001 (主类型 5, 附加信息 9 表示对的个数), 后跟其余 18 个项. 第一个项是第一个键, 第二个项是第一个值, 第三个项是第二个键, 依此类推. 具有重复键的映射可能是格式良好的, 但它不是有效的, 因此会导致不确定的解码; 另见第 3.7 节.
主类型 6: 对其他主类型的可选语义标签. 见第 2.4 节.
主类型 7: 浮点数以及不需要内容的简单数据类型, 还有 "break" 停止码. 见第 2.3 节.
这八种主类型导出一张简单的表, 显示数据项初始字节 256 个可能取值中哪些被使用 (表 5).
在主类型 6 和 7 中, 许多可能的取值保留供未来规范使用. 关于这些取值的更多信息, 见第 7 节.
2.2. 某些主类型的不定长
四种 CBOR 项 (数组、映射、字节串和文本串) 可以使用附加信息值 31 以不定长方式编码. 当数据项的编码需要在该数组或映射内部的项数、或字符串的总长度已知之前就开始时, 这种做法很有用. (其应用在数据项内部通常被称为 "流式处理".)
不定长数组和映射的处理方式与不定长字节串和文本串不同.
2.2.1. 不定长数组和映射
不定长数组和映射只是使用附加信息值 31 打开, 而不指明将包含在该数组或映射中的数据项个数. 初始的主类型和附加信息字节之后跟的是该数组或映射的元素, 与在其他数组或映射中完全一样. 该数组或映射的结束通过在通常应包含下一个数据项的位置编码一个 "break" 停止码来表示. "break" 用主类型 7 和附加信息值 31 (0b111_11111) 编码, 但它本身不是数据项: 它只是一个用于闭合数组或映射的语法特性. 也就是说, "break" 停止码出现在该数组或映射的最后一项之后, 不能出现在其他任何取代数据项的位置. 这样, 不定长数组和映射除了以附加信息值 31 开始、以 "break" 停止码结束之外, 看起来与其他数组和映射完全相同.
具有不定长的数组和映射允许在 "break" 停止码之前给出任意数量的项 (对于数组) 和键/值对 (对于映射). 并不禁止嵌套不定长数组或映射项. 一个 "break" 只终止单个项, 因此嵌套的不定长项所需 "break" 停止码的数量, 恰好等于起始不定长项的类型字节的数量.
例如, 假设某个编码器想要表示抽象数组 [1, [2, 3], [4, 5]]. 其定长编码为 0x8301820203820405:
83 -- Array of length 3 01 -- 1 82 -- Array of length 2 02 -- 2 03 -- 3 82 -- Array of length 2 04 -- 4 05 -- 5
不定长编码可以按需分别应用于该数据项中编码的三个数组中的每一个, 从而得到如下这样的表示:
0x9f018202039f0405ffff 9F -- Start indefinite-length array 01 -- 1 82 -- Array of length 2 02 -- 2 03 -- 3 9F -- Start indefinite-length array 04 -- 4 05 -- 5 FF -- "break" (inner array) FF -- "break" (outer array)
0x9f01820203820405ff 9F -- Start indefinite-length array 01 -- 1 82 -- Array of length 2 02 -- 2 03 -- 3 82 -- Array of length 2 04 -- 4 05 -- 5 FF -- "break" 0x83018202039f0405ff 83 -- Array of length 3 01 -- 1 82 -- Array of length 2 02 -- 2 03 -- 3 9F -- Start indefinite-length array 04 -- 4 05 -- 5 FF -- "break"
0x83019f0203ff820405 83 -- Array of length 3 01 -- 1 9F -- Start indefinite-length array 02 -- 2 03 -- 3 FF -- "break" 82 -- Array of length 2 04 -- 4 05 -- 5
一个不定长映射的示例 (它恰好有两个键/值对) 可能是:
0xbf6346756ef563416d7421ff BF -- Start indefinite-length map 63 -- First key, UTF-8 string length 3 46756e -- "Fun" F5 -- First value, true 63 -- Second key, UTF-8 string length 3 416d74 -- "Amt" 21 -- -2 FF -- "break"
2.2.2. 不定长字节串和文本串
不定长字节串和文本串实际上是零个或多个定长字节串或文本串 ("块") 的拼接, 它们合在一起被视为一个连续的字符串. 不定长字符串以附加信息值 31 和主类型打开, 但其后跟随的是一系列具有定长的字节串或文本串 (即这些块). 该块序列的结束通过在序列中下一个块应出现的位置编码 "break" 停止码 (0b111_11111) 来表示. 各块的内容拼接在一起, 不定长字符串的总长度将是所有块长度之和. 总之, 不定长字符串的编码方式类似于由其各块组成的不定长数组的编码方式, 只是不定长字符串的主类型是 (文本或字节) 字符串的主类型, 并与其各块的主类型相匹配.
对于不定长字节串, 不定长指示符与 "break" 之间的每个数据项 (块) MUST 是定长字节串项; 如果解析器在遇到 "break" 之前看到任何非字节串的项类型, 那就是一个错误.
例如, 假设序列为:
0b010_11111 0b010_00100 0xaabbccdd 0b010_00011 0xeeff99 0b111_11111
5F -- Start indefinite-length byte string 44 -- Byte string of length 4 aabbccdd -- Bytes content 43 -- Byte string of length 3 eeff99 -- Bytes content FF -- "break"
解码之后, 这会得到一个包含七个字节的单个字节串: 0xaabbccddeeff99.
具有不定长的文本串与具有不定长的字节串作用相同, 只是其所有块 MUST 是定长文本串. 注意, 这意味着单个 UTF-8 字符的字节不能分散到不同的块中: 新块只能在字符边界处开始.
2.3. 浮点数与无内容的值
主类型 7 用于两类数据: 浮点数, 以及不需要任何内容的 "简单值". 初始字节中 5 位附加信息的每个取值都有其各自独立的含义, 如表 1 所定义. 与整数的主类型一样, 该主类型的项不携带内容数据; 所有信息都在初始字节中.
| 5 位值 | 语义 |
|---|---|
| 0..23 | 简单值 (值 0..23) |
| 24 | 简单值 (值为后续字节中的 32..255) |
| 25 | IEEE 754 半精度浮点数 (后续 16 位) |
| 26 | IEEE 754 单精度浮点数 (后续 32 位) |
| 27 | IEEE 754 双精度浮点数 (后续 64 位) |
| 28-30 | (未分配) |
| 31 | 用于不定长数据项的 "break" 停止码 |
表 1: 主类型 7 中附加信息的取值
与所有其他主类型一样, 5 位取值 24 表示单字节扩展: 其后跟随一个额外的字节来表示该简单值. (为尽量减少混淆, 只使用 32 到 255 这些值.) 这保持了初始字节的结构: 与其他主类型一样, 其长度始终取决于第一个字节中的附加信息. 表 2 列出了为简单类型分配的和可用的取值.
| 值 | 语义 |
|---|---|
| 0..19 | (未分配) |
| 20 | 假 (False) |
| 21 | 真 (True) |
| 22 | 空 (Null) |
| 23 | 未定义值 (Undefined) |
| 24..31 | (保留) |
| 32..255 | (未分配) |
表 2: 简单值
5 位取值 25、26 和 27 分别用于 16 位、32 位和 64 位 IEEE 754 二进制浮点值. 这些浮点值编码在相应大小的附加字节中. (关于 16 位浮点数的一些信息, 见附录 D.)
2.4. 数据项的可选标签
在 CBOR 中, 数据项可以选择性地以标签为前缀, 以便在保留其结构的同时赋予其额外语义. 标签是主类型 6, 表示一个由该标签整数值所指明的整数; 该 (唯一的) 数据项作为内容数据携带. 如果某个标签要求结构化数据, 则该结构被编码进嵌套的数据项中. 标签的定义通常会限制该标签可以携带何种嵌套数据项.
标签的初始字节遵循正整数 (主类型 0) 的规则. 标签后跟单个任意类型的数据项. 例如, 假设长度为 12 的字节串被打上标签, 以表明它是一个正大数 (第 2.4.2 节). 这将被标记为 0b110_00010 (主类型 6, 附加信息 2 表示标签), 后跟 0b010_01100 (主类型 2, 附加信息 12 表示长度), 再后跟该大数的 12 个字节.
解码器不需要理解标签, 因此在那些创建特定 CBOR 数据项的实现与解码该流的实现都了解数据流中每个项语义含义的应用中, 标签可能价值不大. 标签在本规范中的主要用途是定义诸如日期之类的常见数据类型. 次要用途是在解码器是通用 CBOR 解码器、可能受益于关于项内容的提示时允许可选标签. 理解语义标签对解码器而言是可选的; 它可以直接跳过标签的初始字节, 并解释被打标签的数据项本身.
标签始终应用于紧随其后的项. 因此, 如果标签 A 后跟标签 B, 而标签 B 后跟数据项 C, 则标签 A 应用于将标签 B 应用于数据项 C 的结果. 也就是说, 被打标签的项是由一个标签和一个值组成的数据项. 被打标签项的内容就是被加标签的那个数据项 (即值).
IANA 维护着一个标签值注册表, 如第 7.2 节所述. 表 3 提供了初始值列表, 其定义见本节其余部分.
| 标签 | 数据项 | 语义 |
|---|---|---|
| 0 | UTF-8 字符串 | 标准日期/时间字符串; 见第 2.4.1 节 |
| 1 | 多种 | 基于纪元的日期/时间; 见第 2.4.1 节 |
| 2 | 字节串 | 正大数; 见第 2.4.2 节 |
| 3 | 字节串 | 负大数; 见第 2.4.2 节 |
| 4 | 数组 | 十进制小数; 见第 2.4.3 节 |
| 5 | 数组 | 大浮点数; 见第 2.4.3 节 |
| 6..20 | (未分配) | (未分配) |
| 21 | 多种 | 预期转换为 base64url 编码; 见第 2.4.4.2 节 |
| 22 | 多种 | 预期转换为 base64 编码; 见第 2.4.4.2 节 |
| 23 | 多种 | 预期转换为 base16 编码; 见第 2.4.4.2 节 |
| 24 | 字节串 | 编码的 CBOR 数据项; 见第 2.4.4.1 节 |
| 25..31 | (未分配) | (未分配) |
| 32 | UTF-8 字符串 | URI; 见第 2.4.4.3 节 |
| 33 | UTF-8 字符串 | base64url; 见第 2.4.4.3 节 |
| 34 | UTF-8 字符串 | base64; 见第 2.4.4.3 节 |
| 35 | UTF-8 字符串 | 正则表达式; 见第 2.4.4.3 节 |
| 36 | UTF-8 字符串 | MIME 消息; 见第 2.4.4.3 节 |
| 37..55798 | (未分配) | (未分配) |
| 55799 | 多种 | 自描述 CBOR; 见第 2.4.5 节 |
| 55800+ | (未分配) | (未分配) |
表 3: 标签的取值
2.4.1. 日期和时间
标签值 0 用于遵循 [RFC3339] 中所述标准格式、并经 [RFC4287] 第 3.3 节细化的日期/时间字符串.
标签值 1 用于以 UTC 时间表示相对于 1970-01-01T00:00Z 的秒数的数值表示. (对于可移植操作系统接口 (Portable Operating System Interface, POSIX) 所定义的非负值, 秒数的计数方式与 POSIX 的 "seconds since the epoch" [TIME_T] 相同.) 被打标签的项可以是正整数或负整数 (主类型 0 和 1), 也可以是浮点数 (主类型 7 且附加信息为 25、26 或 27). 注意, 该数字可以为负 (表示 1970-01-01T00:00Z 之前的时刻), 并且如果是浮点数, 还可以表示小数秒.
2.4.2. 大数
大数是指无法放入主类型 0 和 1 所提供的基本整数表示的整数. 它们被编码为一个字节串数据项, 该字节串按网络字节序解释为无符号整数 n. 对于标签值 2, 该大数的值为 n. 对于标签值 3, 该大数的值为 -1 - n. 理解这些标签的解码器 MUST 能够解码带前导零的大数.
例如, 数字 18446744073709551616 (2**64) 表示为 0b110_00010 (主类型 6, 标签 2), 后跟 0b010_01001 (主类型 2, 长度 9), 再后跟 0x010000000000000000 (一个字节 0x01 和八个字节 0x00). 用十六进制表示:
C2 -- Tag 2 29 -- Byte string of length 9 010000000000000000 -- Bytes content
2.4.3. 十进制小数与大浮点数
十进制小数将一个整数尾数与一个以 10 为底的标度因子组合在一起. 当应用需要诸如 1.1 这样的十进制小数的精确表示时, 它们最为有用, 因为在二进制浮点中许多十进制小数都没有精确表示.
大浮点数将一个整数尾数与一个以 2 为底的标度因子组合在一起. 它们是二进制浮点值, 可以超出 CBOR 所支持的三种 IEEE 754 格式 (第 2.3 节) 的范围或精度. 大浮点数也可以被那些需要某种基本二进制浮点能力、而不需要支持 IEEE 754 的受限应用使用.
十进制小数或大浮点数表示为被打上标签的数组, 其中恰好包含两个整数: 一个指数 e 和一个尾数 m. 十进制小数 (标签 4) 使用以 10 为底的指数; 十进制小数数据项的值为 m*(10e). 大浮点数 (标签 5) 使用以 2 为底的指数; 大浮点数数据项的值为 m*(2e). 指数 e MUST 以主类型 0 或 1 的整数表示, 而尾数也可以是大数 (第 2.4.2 节).
十进制小数的一个示例是, 数字 273.15 可以表示为 0b110_00100 (主类型 6 表示标签, 附加信息 4 表示标签类型), 后跟 0b100_00010 (主类型 4 表示数组, 附加信息 2 表示数组长度), 后跟 0b001_00001 (主类型 1 表示第一个整数, 附加信息 1 表示值 -2), 后跟 0b000_11001 (主类型 0 表示第二个整数, 附加信息 25 表示两字节值), 再后跟 0b0110101010110011 (两字节的 27315). 用十六进制表示:
C4 -- Tag 4 82 -- Array of length 2 21 -- -2 19 6ab3 -- 27315
大浮点数的一个示例是, 数字 1.5 可以表示为 0b110_00101 (主类型 6 表示标签, 附加信息 5 表示标签类型), 后跟 0b100_00010 (主类型 4 表示数组, 附加信息 2 表示数组长度), 后跟 0b001_00000 (主类型 1 表示第一个整数, 附加信息 0 表示值 -1), 再后跟 0b000_00011 (主类型 0 表示第二个整数, 附加信息 3 表示值 3). 用十六进制表示:
C5 -- Tag 5 82 -- Array of length 2 20 -- -1 03 -- 3
十进制小数和大浮点数不提供 Infinity、-Infinity 或 NaN 的表示; 如果在需要使用十进制小数或大浮点数的地方需要这些值, 可以使用第 2.3 节中的 IEEE 754 半精度表示. 对于受限应用, 当在将特定数字表示为整数与表示为十进制小数或大浮点数之间存在选择时 (例如指数很小且非负时), 存在一种实现质量上的期望, 即直接使用整数表示.
2.4.4. 内容提示
本节中的标签用于通用 CBOR 处理器可能使用的内容提示.
2.4.4.1. 编码的 CBOR 数据项
有时, 携带一个内嵌的 CBOR 数据项是有益的, 该数据项并不打算在解析外层数据项时被立即解码. 标签 24 (CBOR 数据项) 可用于将内嵌的字节串标记为以 CBOR 格式编码的数据项.
2.4.4.2. 供 CBOR 到 JSON 转换器使用的预期后续编码
标签 21 到 23 表示在与基于文本的表示进行互操作时, 字节串可能需要特定的编码. 当编码器知道它所写入的字节串数据很可能稍后被转换为某种特定的基于 JSON 的用途时, 这些标签很有用. 那种用途规定某些字符串按 base64、base64url 等方式编码. 编码器使用字节串而不是自行进行编码, 是为了减小消息大小、减小编码器的代码大小, 或两者兼有. 编码器并不知道转换器是否为通用转换器, 因此希望说明它认为将二进制串转换为 JSON 的正确方式是什么.
被打标签的数据项可以是字节串, 也可以是任何其他数据项. 在后一种情况下, 该标签应用于该数据项中包含的所有字节串数据项, 但被带有预期转换标签的嵌套数据项所包含的那些除外.
这三种标签类型建议转换为 [RFC4648] 中定义的三种基础数据编码. 对于 base64url 编码, 不使用填充 (见 RFC 4648 第 3.2 节); 也就是说, 所有末尾的等号 ("=") 都从 base64url 编码的字符串中移除. 后续可能会为 RFC 4648 的其他数据编码、或为在字符串中编码二进制数据的其他方式定义标签.
2.4.4.3. 编码的文本
有些文本串所承载的数据具有在互联网上广泛使用的格式, 有时解码器可以验证这些格式并以适当形式呈现给应用. 其中一些格式有对应的标签.
-
标签 32 用于 URI, 如 [RFC3986] 所定义;
-
标签 33 和 34 用于 base64url 和 base64 编码的文本串, 如 [RFC4648] 所定义;
-
标签 35 用于 Perl 兼容正则表达式 (Perl Compatible Regular Expressions, PCRE) / JavaScript 语法 [ECMA262] 的正则表达式.
-
标签 36 用于 MIME 消息 (包括所有头部), 如 [RFC2045] 所定义;
注意, 标签 33 和 34 与 21 和 22 的区别在于: 前者以 base 编码形式传输数据, 而后者以原始字节串形式传输数据.
2.4.5. 自描述 CBOR
在许多应用中, 从上下文可以清楚地看出正在使用 CBOR 来编码某个数据项. 例如, 某个特定协议可能规定使用 CBOR, 或者指出了某个规定使用 CBOR 的媒体类型. 然而, 也可能存在无法获得此类上下文信息的应用, 例如当 CBOR 数据存储在文件中且没有使用用于消歧的元数据时. 此时, 让数据本身具备一些可区分的特征可能会有所帮助.
标签 55799 就是为此目的而定义的. 它不对其后的数据项施加任何特殊语义; 也就是说, 被打上标签 55799 的数据项的语义与数据项本身的语义完全相同.
该标签的序列化为 0xd9d9f7, 它似乎未被用作常用文件类型的区分标记. 特别地, 如果其后跟一个有效的 CBOR 数据项, 它在任何 Unicode 编码中都不是 Unicode 文本的有效开头.
例如, 某个解码器可能既能解析 CBOR 又能解析 JSON. 这样的解码器需要机械地区分这两种格式. 编码器帮助解码器的一个简便方法是用标签 55799 标记整个 CBOR 项, 该标签的序列化永远不会出现在 JSON 文本的开头.