xar file

Growl application のドキュメントを見ていると plist に通知する notification を記述するとある。

http://growl.info/documentation/developer/implementing-growl.php?lang=cocoa

実例を見ようと思い Growl の package の中身参照してみようと思ったが、.pkg なので、専用のフォーマットがあるかと思ったが、file で見ると xar と出てくる。このフォーマットは最近の OS X や RPM5 などでパッケージに使われるフォーマットのようだ。

簡単にいうと先頭に 28 byte のヘッダがあり、その後に gzip で圧縮された xml 形式の table of contents 情報がある。
table of contents の位置は 28 byte でサイズは toc_length_compressed でわかる。利点としては content ごとに異なる encoding を使用できたり、チェックサムを圧縮前、後に関して保持できたり、大きなアーカイブでも扱いやすかったりといった特徴があげられている。XAR の X は XML の X ではなく eXtensible の X で, XMLtoc を拡張することで含める metadata を追加できる。header が固定フォーマットの tar, cpio に対して柔軟だ。tar, cpio はファイル全体を読まないといけないという難点もある。

table of contents (toc) は次のようにして見ることができる。

xar --dump-toc=- -f h.pkg

中身をざっと眺める python script を書いてみたが、あっさりと動いてくれた。プロトタイプなので、エラーハンドリングはほとんどなし。


#
# decode pkg (xar) header
#

import os, sys, struct
import zlib

# struct xar_header {
# uint32_t magic;
# uint16_t size;
# uint16_t version;
# uint64_t toc_length_compressed;
# uint64_t toc_length_uncompressed;
# uint32_t cksum_alg;
# };

XAR_HEADER_SIZE = 4 + 2 + 2 + 8 + 8 + 4

def dump_content(path, fname, offset, length, enc):
"""dump content in xar file
args:
path: path to xar file
fname: content file name
offset: offset in compressed data in xar file
lenfth: length of compressed data
enc: encoding, currently application/x-gzip only
"""
fd = os.open(path, os.O_RDONLY)
os.lseek(fd, offset, 0)
compressed_data = os.read(fd, length)
print zlib.decompress(compressed_data)
os.close(fd)

def process_file(path):

f = open(path)
header_string = f.read(XAR_HEADER_SIZE)
print "header_string length %d" % len(header_string)
fmt = "!IHHQQI"
(magic, size, version, toc_length_compressed, toc_length_uncompressed,
checksum_alg) = struct.unpack(fmt, header_string)

print "magic: %#lx" % magic
print "size: %#d %#x" % (size, size)
print "version: %#d %#x" % (version, version)
print "toc_length_compressed: %#d %#x" % (toc_length_compressed,
toc_length_compressed)
print "toc_length_uncompressed: %#d %#x" % (toc_length_uncompressed,
toc_length_uncompressed)
print "checksum_alg: %#d %#x" % (checksum_alg, checksum_alg)

toc_compressed = f.read(toc_length_compressed)

tocs = zlib.decompress(toc_compressed)

from xml.dom import minidom

doc = minidom.parseString(tocs)

files = doc.getElementsByTagName("file")
# print files

for file in files:
fname = file.getElementsByTagName("name")[0].firstChild.data
data = file.getElementsByTagName("data")
length = int(data[0].getElementsByTagName("length")[0].firstChild.data)
offset = int(data[0].getElementsByTagName("offset")[0].firstChild.data)
size = int(data[0].getElementsByTagName("size")[0].firstChild.data)
enc = data[0].getElementsByTagName("encoding")[0].getAttribute("style")
print "%s: offset %d len %d size %d %s" % (fname, offset, length, size, enc)

offset_in_file = XAR_HEADER_SIZE + toc_length_compressed + offset
dump_content(path, fname, offset_in_file, length, enc)

f.close()

def main(args):
for f in args:
if os.path.exists(f):
process_file(f)

if __name__ == '__main__':
main(sys.argv[1:])


pkg ファイルが package の中身であるアプリケーションなどを直接 xar で保持しているかというと層でもなく、pkg の中身は次のようになっていた。

  • Bom
  • PackageInfo
    • XML での package 情報
  • Payload
    • gzip 圧縮されたファイルの cpio archive
  • Scripts
    • gzip 圧縮された postinstall, postupgrede, preflight などのスクリプトの cpio archive