rust-plist
A Rust library for reading and writing Apple property list (plist) files in XML, binary, and ASCII formats, with full Serde support.
Repository Health
Technical Analysis
plist is a Rust crate for working with Apple’s property list format, the XML- and binary-encoded structured data files used throughout macOS, iOS, and Apple’s development toolchains (Info.plist, entitlements, preference files, and more). It supports reading and writing all three plist encodings — XML, binary, and Apple’s ASCII/OpenStep format — through a single unified Value type that mirrors the format’s native array, dictionary, boolean, data, date, real, integer, string, and UID variants.
Beyond the untyped Value API, the crate ships an optional serde integration (enabled by default) that lets consumers derive Serialize/Deserialize on their own Rust structs and read or write plists directly into and out of typed data, avoiding manual dictionary traversal. It also provides plist! and plist_dict! macros for constructing values inline, and dedicated readers/writers for streaming large plists without loading the entire structure into memory at once.
What You Get
- A
Valueenum covering every plist primitive (array, dictionary, boolean, data, date, real, integer, string, UID) with format-agnostic read/write methods - Automatic format detection when reading —
Value::from_file/from_readerfigure out whether a plist is XML, binary, or ASCII encoded - Optional Serde integration for deriving typed structs directly from plist data instead of manually walking a
Dictionary - Dedicated binary, XML, and ASCII readers/writers under the
streammodule for lower-level or streaming access plist!andplist_dict!macros for buildingValuetrees inline in Rust code- A
Dictionarytype (ordered, IndexMap-backed) that preserves key insertion order, matching how real plist files are structured
Common Use Cases
- Parsing an app’s
Info.plistto read bundle identifiers, version strings, and permission usage descriptions during macOS/iOS build tooling - Reading and validating iOS/macOS entitlements or provisioning profile plists in CI or code-signing pipelines
- Serializing Rust configuration structs to XML or binary plist for tools that interoperate with Apple’s ecosystem (defaults, launchd jobs, mobileconfig profiles)
- Building cross-platform Rust utilities that need to inspect or generate macOS preference files without shelling out to
plutilordefaults
Under The Hood
Architecture
The crate is organized around a single format-agnostic Value enum in src/value.rs that all three on-disk plist encodings ultimately read into and write from. Format-specific parsing and serialization live under src/stream/ (ascii_reader.rs, binary_reader.rs, binary_writer.rs, xml_reader.rs, xml_writer.rs), each implementing a shared streaming Event-based reader/writer interface defined in stream/mod.rs; Value::from_reader auto-detects encoding and dispatches to the right reader, while Value::from_reader_xml/from_reader_ascii bypass detection for callers who already know the format. Serde support is layered on top in de.rs/ser.rs, translating the Event stream into/from serde::Deserialize/Serialize calls rather than requiring a second parse pass, so typed and untyped access share the same underlying format code. A dedicated error.rs centralizes all failure modes (per-format syntax errors, unexpected event types, I/O errors) into one Error type carrying an optional file position, giving callers precise diagnostics regardless of which encoding tripped the error.
Tech Stack
The crate targets Rust edition 2021 with an MSRV of 1.88, and keeps its dependency surface deliberately small: quick-xml 0.41 handles low-level XML tokenizing for the XML reader/writer, base64 0.22 encodes/decodes the Data variant, time 0.3 (with parsing/formatting features) backs the Date type, indexmap 2.1 gives Dictionary its order-preserving map semantics, and serde 1.0 is an optional dependency gated behind the (default-on) serde feature. There is no runtime, no async, and no I/O abstraction beyond std::io::{Read, Write, Seek} — the crate is a pure, synchronous parsing/serialization library intended to be linked into other tools.
Code Quality
The crate enforces #![deny(warnings)] and denies broken rustdoc intra-doc links at the crate root, signaling a low tolerance for drift. Testing is extensive: serde_tests.rs alone runs to over a thousand lines covering typed round-trips, and tests/ adds fixture-driven tests against real .plist files (tests/data/), trybuild-based UI tests asserting macro compile errors (tests/ui/), and macrotest expansion tests (tests/expand/) that check the exact macro-expanded output of plist!/plist_dict!. A fuzz/ directory with its own Cargo.toml indicates the binary and XML readers are fuzz-tested against malformed input, which matters for a crate that parses untrusted files. Errors are modeled as an explicit typed Error/ErrorKind rather than panics or string errors, and a CHANGELOG following Keep a Changelog conventions tracks every release.
API Design
The public surface is small and consistent: Value::from_file/from_reader pair with to_file_xml/to_file_binary/to_writer_xml/to_writer_binary for the untyped path, while from_file/from_bytes/from_reader and to_file_xml/to_file_binary (Serde variants) mirror the same shape for typed structs — so learning one half of the API largely teaches the other. The Dictionary type exposes a familiar map-like interface (get, iteration, insertion order) rather than inventing new vocabulary, and the crate’s own doc comments include runnable examples for both the Value and Serde entry points. Advanced/streaming APIs are intentionally hidden behind the enable_unstable_features_that_may_break_with_minor_version_bumps feature flag, keeping the default public API stable across minor versions while still giving power users an escape hatch — a deliberate, well-documented trade-off between ergonomics and flexibility.