了解Apple的二进制属性列表格式

标记字节有时足以完全识别一个对象。 例如,空值的标记等于零,布尔值的标记为0(如果为false),如果标记为0,则为0x09(如果为true)。 所有其他对象都可以通过其4个最高有效位来唯一标识(从现在开始,我将使用最左和最右的术语代替MSB和LSB)。 例如,整数的最左4位是0001(0x1),而对于字符串,它们是0110(0x5)。

剩下的最右边的4位表示大小信息,即,此类型的实际值将在标记之后占用多少字节。 在某些情况下,如果对象足够小,则会立即在最右边的4位中对大小进行编码。 例如,ASCII字符串“ Hello”将被编码为0x55,然后将跟随实际的字符值。 在其他情况下,填充标记(0x0F)与对象标记进行“或”运算,表示下一个字节在实际值字节之前编码大小信息。 更具体地说,如果标记的最右4位等于1111(0xF),则下一个字节将具有以下结构:

  • 其最左边的4位等于0001(0x1)
  • 它的最右边的4位告诉我们编码对象大小需要多少字节。 如果最右边的4位包含值x ,则大小将需要pow(2,x)字节

然后,跟随pow(2,x)个字节,应该以大字节序读取该字节,以提供对象的实际大小。 之后,将遵循对象的实际值。 例如,字符串“ This is a long string”包含21个ASCII字符。 标记为0x5F,后跟字节0x10 (因为pow(2,0)= 1,并且1个字节足以编码值21),然后是0x15 (十六进制的十进制21),然后是21个字符其他。

对应于诸如整数,实数,字符串之类的对象的标记紧随其后的是代表其实际值的多字节序列(例如,如上所述的单个字符串字符)。 但是,并非总是如此。 对于对象容器,例如数组和字典,标记字节后跟对象引用 只是偏移表的偏移量(请参阅下一节)。 这样的偏移量是object_ref_size 由bplist尾部确定的长字节,从偏移量表的开头开始计数。 因此,容器元素只是大小为object_ref_size的引用,它指向偏移表中的某个位置,该位置本身就是offset_table_offset_size 字节长,指向对象表,特别是指向与单个对象相对应的标记。 下一节中的示例将消除所有混淆。

此技术将实际的多层结构平面化,并允许所有对象具有固定的大小。 因此,我们始终知道,值0xA5的标记后面是5 * object_ref_size字节。 这种间接级别也允许基本的压缩形式。 当容器的值完全相同时,它们可能指向相同的偏移表偏移量。

容器示例:

0xA5 — 5个元素的数组。 在标记后没有立即找到这5个元素的值。 相反,在标记之后,我们找到5个对象引用,它们充当偏移表的偏移量。 遵循这些引用后,可以在对象表中找到其各自的字节标记的偏移量,在该表中可以找到实际值(或者如果标记再次是容器,则遵循相同的过程)。

0xAF 0x10 0x0F —由15个元素组成的数组(与上面相同,但是现在大小信息不适合4位)。 随后是15个对象引用。

0xD6 —由6个键值对组成的字典。 在标记之后没有立即找到这6个键值对的实际值。 相反,在标记之后,我们找到12个对象引用,它们充当偏移表的偏移量。 首先,我们找到6个键偏移,然后将6个值偏移分组在一起。 在引用之后,可以在对象表中找到其各自的字节标记的偏移量,在该表中可以找到实际值。 (或者,如果标记又是容器,则遵循相同的步骤)。

偏移表

第三部分包含对象表的偏移量,并作为一种指导我们了解对象实际值的方法。 每个偏移量为offset_table_offset_size 由bplist 尾部确定的字节长,并且指向对象表中字节标记的位置。 从文件的开头(而不是标头的结尾)计算偏移量。 偏移量表包含num_objects个 偏移量,表示对象表中实际编码了多少个对象。 请记住,某些项目可能会被压缩(编码一次并重复使用),因此当您查看plist的可读内容时,您可能会看到比num_objects更多的内容

预告片

预告片长32个字节,并包含大小信息。 字节0至4未使用,而字节5包含排序版本。

字节6告诉我们每个偏移量表offset( offset_table_offset_size )需要多少个字节。 如果plist很大,则从偏移表跳转到对象表中的标记可能需要2个或更多字节。 毕竟,一个字节只能从文件开头占用我们255个字节,如果对象数量很大,这可能还不够。

类似地,字节7告诉我们容器中每个对象引用( object_ref_size )需要多少个字节 同样,如果plist很大,则从对象表跳转到偏移表中的某个位置可能需要2个或更多字节。

字节8到15包含已编码的对象数( num_objects )。 请记住,多字节数字值被编码为big endian。

top_object_offset (字节16到23)告诉我们从偏移表开始的偏移(通常为零,表示第一项)。 偏移表的此位置包含对象表中第一个标记的偏移。

offset_table_start (字节24到31)表示偏移表的开始,从文件的开始算起。

考虑下面的简单plist,从Xcode中可以看到:

其XML表示如下:

图3显示了相同的二进制格式的plist,其中标头(绿色),对象表(蓝色),偏移表(红色)和尾部(黄色)被描绘。

从标头的前8个字节开始,我们立即将plist类型标识为bplist00。 现在,让我们仔细看看预告片:

我们可以立即推断出以下内容(请记住,所有内容都是大字节序):

  • offset_table_offset_size :0x01字节(绿色)
  • object_ref_size :0x01字节(黄色)
  • num_objects :0x10(16个对象,橙色)
  • top_object_offset :0x00(红色)
  • offset_table_start :0x7F(蓝色)

接下来,让我们看一下偏移表。 从预告片中,我们已经知道偏移量表的起始位置(0x7F),它指向多少个对象(0x10),偏移量表插槽的大小(1个字节)以及第一个对象指针位于哪个偏移量(位置0) )。

每个偏移量表条目均包含一个偏移量,该偏移量可将我们引向对象表中其匹配的标记(下面以相同的颜色🤯🧐显示):

让我们关注标记(…并放下多色):

一目了然,我们可以依次看到以下16个对象:

  1. 0xD3 —具有3个键值对的字典
  2. 0x57 —具有7个字符的ASCII字符串
  3. 0x56 —具有6个字符的ASCII字符串
  4. 0x5B —具有11个字符的ASCII字符串
  5. 0x23 —长度为8字节的实数
  6. 0xA2 — 2个元素的数组
  7. 0xD2 — 2个键值对的字典
  8. 0x56 —具有6个字符的ASCII字符串
  9. 0x5A —具有10个字符的ASCII字符串
  10. 0x09 —布尔真实值
  11. 0x33 —日期
  12. 0xD2 — 2个键值对的字典
  13. 0x5A —具有10个字符的ASCII字符串
  14. 0x08 —布尔值false
  15. 0x33 —日期
  16. 0x5D —具有13个字符的ASCII字符串

让我们跟随第一个对象的路径。 查看预告片,第一个偏移表偏移量从偏移表的起始位置为零,因此我们看一下偏移表的起始位置(0x7F)。 包含的值(0x08)告诉我们第一个对象位于文件开头之后8个字节。 那里的值是(0xD3),表示具有3个键值对的字典。 在下一个位置(0x09),启动键参考(不是实际的键)。 这三个键是偏移表的偏移量。 偏移量为0x01、0x02、0x03。 之后,将显示3个值。 它们还是偏移表的偏移量:0x04、0x05、0x0F。

让我们找到字典的第一把钥匙。 我们从0xD3标记移动1个位置,找到值0x01。 然后,我们以偏移量表开头(0x7F)并将此值相加,得出0x80。 在0x80处,我们找到值0x0F,该值将我们带到标记0x57,表示一个具有7个字节的字符串:

0x56 0x65 0x72 0x73 0x69 0x6F 0x6E

产生字典键“ Version”。

为了获得其值,我们从0xD3标记向右移3个位置,并遵循相同的步骤,得到标记8x实数,即0x23。

它的值是从big endian中读取的接下来的8个字节计算得出的:

0x40 0x22 0xD1 0xEB 0x85 0x1E 0xB8 0x52

对应于IEEE754双精度64位格式的9.41。 因此,我们的第一个键值对显示为: “版本”:9.41

让我们继续第二个键,它包含值0x02并将我们引向标记0x56。 标记表示一个ASCII字符串,后接6个字符,并产生键“电子邮件”。

相应的值生成标记0xA2,该标记是2个元素的数组:

接下来的两个字节包含指向偏移表的对象引用(遵循绿色和白色路径):

每个偏移表偏移最终指向2个键值对的字典。 让我们看一下第一个字典:

第一个键指向标记0x56和字符串“ isRead”

第二个键指向标记0x5A和字符串“ receivedAt”

第一个值指向标记0x09,它是一个值为true的布尔值。

第二个值指向标记0x33,该日期是具有8字节核心数据时间戳记的日期,该时间戳记对应于当地时间2018年1月14日星期日UTC或2018年1月14日星期日20:18:26 Sun UTC 14在Xcode中看到。

现在,您应该能够应用相同的模式对0x57处的最终字典进行解码。

值得观察的是,字典键“ isRead”对于两个词典仅保存一次,而另一个键“ receivedAt”则被保存两次,尽管它们是完全相同的字符。 如果计算XML表示形式的人类可读元素,则将找到17个对象,而二进制版本包含16个编码对象。 区别在于压缩的“ isRead”键。

结论

二进制文件复制器提供了一种紧凑的方式,可以以结构化和可移动的方式有效地存储对象。 在此博客文章中,我们窥视了最常见的bplist版本00的内部结构。