A minimal-dependency Common Lisp parser for Apple binary property lists (the
bplist00 format produced by CFBinaryPlist and plutil -convert binary1).
Layered on top of the core parser are an NSKeyedArchiver unarchiver, a Safari
.webarchive resource extractor, a human-readable structure dumper, and a
standalone bplist-dump command-line tool.
The parser is written to survive hostile input: every length/count field is
bounds-checked before allocation, nesting depth and object/byte budgets are
capped, reference cycles are detected, and only a single plist-error
condition type ever escapes the public entry points.
Dependencies are kept to a minimum: babel
for portable UTF-8/UTF-16 string decoding, and
ieee-floats for a float-decode
fallback used only on non-SBCL implementations (on SBCL it is never called).
Dependencies are managed with ocicl:
ocicl installThen load the system:
(asdf:load-system :apple-binary-plist) ; nickname: abplist(abplist:parse-file #p"settings.plist") ; => the top-level Lisp value
(abplist:parse bytes) ; from an (unsigned-byte 8) vector
(abplist:parse-stream stream) ; from a byte streamplist types map onto Lisp values as follows:
| plist type | Lisp representation |
|---|---|
| null/true/false | +null+ / +true+ / +false+ (keywords) |
| integer | integer |
| real | double-float / single-float |
| date | plist-date struct (Cocoa 2001 epoch) |
| data | (simple-array (unsigned-byte 8) (*)) |
| string | string |
| uid | plist-uid struct |
| array | simple-vector |
| set | list |
| dictionary | hash-table (:test #'equal) |
Use plist-date-universal-time to convert a plist-date to Common Lisp
universal time.
dump/dump-file print an indented, type-annotated tree, eliding long strings
and data blobs so the output stays scannable. Here is a real dump of the sample
.webarchive committed under test/ (a saved copy of this project's own GitHub
page), trimmed to its first subresource:
(abplist:dump-file #p"test/example.webarchive")dict (2 entries)
"WebMainResource" => dict (5 entries)
"WebResourceData" => data[362290] 3C 21 44 4F 43 54 59 50 45 20 68 74 6D 6C 3E 3C ...
"WebResourceFrameName" => string[0] ""
"WebResourceMIMEType" => string[9] "text/html"
"WebResourceTextEncodingName" => string[5] "UTF-8"
"WebResourceURL" => string[45] "https://github.com/lispnik/apple-binary-plist"
"WebSubresources" => array (128 elements)
[0] dict (4 entries)
"WebResourceData" => data[76753] 5B 64 61 74 61 2D 63 6F 6C 6F 72 2D 6D 6F 64 65 ...
"WebResourceMIMEType" => string[8] "text/css"
"WebResourceResponse" => data[2251] 62 70 6C 69 73 74 30 30 D4 01 02 03 04 05 06 07 ...
"WebResourceURL" => string[65] "https://github.githubassets.com/assets/light-62b06818b06b09b..."
... 127 more subresources ...
Note that each WebResourceResponse blob is itself a binary plist — its data
begins with 62 70 6C 69 73 74 30 30, i.e. bplist00. It is an
NSKeyedArchiver-encoded NSHTTPURLResponse you can decode with unarchive.
unarchive walks an archive's flat $objects table, follows the UID
references, and rebuilds the natural nested object graph. Foundation container
and value classes (NSDictionary, NSArray, NSString, NSDate, NSURL,
NSUUID, ...) are decoded to native values; unknown classes are preserved as
archived-object structs.
(abplist:unarchive-file #p"archive.plist")
(abplist:unarchive bytes :root "someTopKey")
(abplist:keyed-archive-p (abplist:parse bytes)) ; recogniser(abplist:extract-webarchive #p"page.webarchive" #p"out/")
;; => list of extracted-resource structs; each WebResourceData blob is written
;; into out/ under a reconstructed host/path tree.By default a write that would escape the output directory (e.g. via a planted
symlink) is refused. See the docstring for :flatten, :max-files,
:if-exists, and :follow-symlinks.
Build the standalone executable with the Makefile (which calls ASDF's
program-op):
make # or: sbcl --non-interactive --eval '(asdf:make :apple-binary-plist/cli)'
# => bin/bplist-dumpUsage: bplist-dump [options] FILE
-u, --unarchive decode an NSKeyedArchiver graph before dumping
-s, --max-string N truncate strings to N characters (default 60)
-b, --max-bytes N truncate data blobs to N bytes (default 16)
-x, --extract DIR extract .webarchive WebResourceData blobs into DIR
--flatten with --extract, write all files directly into DIR
--no-overwrite with --extract, refuse to overwrite existing files
--max-files N with --extract, refuse archives with over N resources
-h, --help show this help and exit
-v, --version show version and exit
With no FILE, or FILE of "-", the plist is read from standard input.
sbcl --non-interactive --eval '(asdf:test-system :apple-binary-plist)'The core library runs on any Common Lisp with full (21-bit) Unicode
characters — SBCL, CCL, ECL, ABCL, Clasp, LispWorks. String decoding goes
through babel and float decoding through ieee-floats (SBCL uses a faster
native sb-kernel path), so no implementation-specific code is required to
parse. CLISP works only if built with wide characters, since UTF-16 and astral
code points need them. CI runs the full suite on SBCL, CCL, and ECL across
Linux and macOS (CCL on Linux only — no CCL binary exists for arm64 macOS).
The bplist-dump CLI reads standard input through a fast sb-sys path on SBCL
and through /dev/stdin on other implementations, so piping works across Lisps
on macOS and Linux. There is no Windows stdin path — pass a FILE argument there
instead of piping.
MIT. See LICENSE.