//! Property lists: shared, tree-structured data.
//!
//! ## Rust rewrite notes
//!
//! This implementation should be good enough to facilitate migrating from C to
//! Rust and trasitioning away from using property lists everywhere instead of
//! proper structs. `PropList`s are a really cool general-purpose tool for
//! attaching data to objects and persisting it to disk. But in Rust, it is
//! easier to use proper structs with typed fields and appropriate `#[derive]`
//! declarations to generate code for serialization and deserialization
//! (presumably using Serde).
//!
//! As code that uses `PropList`s is rewritte in Rust, we should work on
//! migrating away from use of `PropList`s. Objects whose fields that can be
//! statically typed should be represented as structs. They may still be
//! persisted by cramming them into `PropList`s and writing those to disk, but
//! it would be better still to implement Serde-based serialization to and from
//! the property list format.
//!
//! The `PropList` implementation itself can also be improved substantially. See
//! [`PropList`] for thoughts on this.

use atomic_write_file::unix::OpenOptionsExt;

use std::{
    cell::RefCell,
    collections::{hash_map, HashMap},
    ffi::{CString, OsStr, OsString},
    fmt, hash,
    io::{self, BufWriter, Write},
    path::Path,
    process::Command,
    ptr,
    rc::Rc,
};

use crate::find_file;

pub mod parser;
pub mod writer;

/// Payload of a [`PropList`].
#[derive(Eq, PartialEq)]
pub enum Node {
    /// Text data. This is UTF-8 encoded and null-safe.
    ///
    /// ## Rust rewrite notes
    ///
    /// It would be better for this to be a `String`, but the C interface
    /// requires borrows of C-style strings.
    String(CString),
    /// Array of child `PropList`s.
    Array(Vec<PropList>),
    /// `PropList`-keyed table of child `PropList`s. Keys should only have
    /// `Node::String` or `Node::Data` payloads, although there is almost no
    /// enforcement of this.
    Dictionary(HashMap<PropList, PropList>),
}

impl hash::Hash for Node {
    fn hash<H: hash::Hasher>(&self, h: &mut H) {
        match self {
            Node::String(s) => s.hash(h),
            Node::Array(a) => {
                for p in a {
                    p.hash(h);
                }
            }
            Node::Dictionary(d) => {
                for (k, v) in d {
                    k.hash(h);
                    v.hash(h);
                }
            }
        }
    }
}

impl fmt::Debug for Node {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}",
            writer::Display {
                inline: writer::Inline::Soft,
                clear_left: false,
                indentation: 0,
                increment: 2,
                node: self,
            }
        )
    }
}

fn merge_shallow(dest: PropList, source: PropList) {
    if ptr::eq(dest.0.as_ref(), source.0.as_ref()) {
        return;
    }
    let Node::Dictionary(ref mut dest_items) = *dest.0.borrow_mut() else {
        return;
    };
    let Node::Dictionary(ref source) = *source.0.borrow() else {
        return;
    };
    for (k, v) in source {
        if ptr::eq(dest.0.as_ptr(), k.0.as_ptr()) {
            // Don't borrow k if it is already borrowed as dest.
            continue;
        }
        dest_items.insert(k.clone(), v.clone());
    }
}

fn merge_deep(dest: PropList, source: PropList) {
    if ptr::eq(dest.0.as_ptr(), source.0.as_ptr()) {
        return;
    }
    let Node::Dictionary(dest_items) = &mut *dest.0.borrow_mut() else {
        return;
    };
    let Node::Dictionary(source_items) = &*source.0.borrow() else {
        return;
    };

    for (key, value) in source_items {
        if key.0.try_borrow().is_err() || value.0.try_borrow().is_err() {
            // Something has already borrowed key or value. This may happen if
            // source contains pointers that are also in dest, or if dest is
            // cyclic. This is bad, but we just bail out.
            continue;
        }
        match dest_items.entry(key.clone()) {
            hash_map::Entry::Vacant(v) => {
                // Dest has nothing at key. Insert value from source.
                v.insert(value.clone());
            }
            hash_map::Entry::Occupied(mut o) => {
                let recur = match *o.get().0.borrow() {
                    Node::Dictionary(_) => true,
                    _ => false,
                };
                if recur {
                    // dest[key] is a dictionary. Recur on dest[key] and value from source.
                    merge_deep(o.get().clone(), value.clone());
                } else {
                    // dest[key] is not a dictionary. Overwrite with value from source.
                    o.insert(value.clone());
                }
            }
        }
    }
}

fn subtract_shallow(dest: PropList, source: PropList) {
    if ptr::eq(dest.0.as_ptr(), source.0.as_ptr()) {
        if let Node::Dictionary(ref mut items) = *dest.0.borrow_mut() {
            items.clear();
        }
        return;
    }

    let Node::Dictionary(ref mut dest_items) = *dest.0.borrow_mut() else {
        return;
    };
    let Node::Dictionary(ref source_items) = *source.0.borrow() else {
        return;
    };
    for (k, v) in source_items.iter() {
        if ptr::eq(dest.0.as_ptr(), k.0.as_ptr()) {
            continue;
        }
        if let hash_map::Entry::Occupied(o) = dest_items.entry(k.clone()) {
            if o.get() == v {
                o.remove();
            }
        }
    }
}

fn subtract_deep(dest: PropList, source: PropList) {
    if ptr::eq(dest.0.as_ptr(), source.0.as_ptr()) {
        if let Node::Dictionary(ref mut items) = *dest.0.borrow_mut() {
            items.clear();
        }
        return;
    }

    let Node::Dictionary(ref mut dest_items) = *dest.0.borrow_mut() else {
        return;
    };
    let Node::Dictionary(ref source_items) = *source.0.borrow() else {
        return;
    };
    for (k, v) in source_items.iter() {
        if ptr::eq(dest.0.as_ptr(), k.0.as_ptr()) {
            continue;
        }
        if let hash_map::Entry::Occupied(o) = dest_items.entry(k.clone()) {
            if o.get() == v {
                o.remove();
                continue;
            }
            let recur = match (&*o.get().0.borrow(), &*v.0.borrow()) {
                (Node::Dictionary(_), Node::Dictionary(_)) => true,
                _ => false,
            };
            if recur {
                subtract_deep(o.get().clone(), v.clone());
            }
        }
    }
}

/// Data graph with convenient (de)serialization to/from the [property
/// list](https://en.wikipedia.org/wiki/Property_list) format.
///
/// ## Rust rewrite notes
///
/// The original WUtils `PropList` was a reference-counted pointer, so it
/// supported shallow copy and shared-memory semantics that we have continued to
/// try to support in the Rust implementation. As a result, `PropList` is a thin
/// wrapper around an `Rc<RefCell<Node>>`. There are several reasons why this is
/// probably unnecessary and something we should migrate away from:
///
/// * It allows for the creation of non-tree structures, which was probably
///   never intended. (A degenerate `PropList` could even have itself as a child.)
/// * It complicates recursive operations on `PropList`s (equality checks,
///   merging, or taking differences) because a given `PropList` may occur
///   multiple times when traversing two `PropList`s, but `Rc` only allows it to be
///   mutably borrowed once.
/// * Allowing subtrees to be shared between two different `PropList`s
///   may lead to spooky action at a distance and may not actually be taken
///   advantage of by any client code.
///
/// As client code is migrated into Rust, it would be great to move away from
/// this implementation to a simpler one. As discussed in the module-level
/// rewrite notes, we may even be able to do away with `PropList` itself
/// (perhaps in favor of using Serde to write to and from property list files on
/// disk).
#[derive(Clone)]
pub struct PropList(Rc<RefCell<Node>>);

impl PropList {
    pub fn new(node: Node) -> Self {
        PropList(Rc::new(RefCell::new(node)))
    }

    /// Reads `r` to the end and tries to parse it into a `PropList`.
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<PropList, String> {
        let path = path.as_ref().to_path_buf();
        let buf = std::fs::read_to_string(&path).map_err(|e| format!("{}", e))?;
        parser::from_str(buf.as_str())
    }

    // Runs `command` and tries to parse a PropList from its standard output.
    pub fn from_command<S: AsRef<OsStr>>(command: S) -> Result<PropList, String> {
        let command: OsString = command.as_ref().to_os_string();
        let output = Command::new("/bin/sh")
            .arg("-c")
            .arg(command.clone())
            .output()
            .map_err(|e| format!("{}", e))?;
        let output = str::from_utf8(&output.stdout).map_err(|e| format!("{}", e))?;
        parser::from_str(&output)
    }

    pub fn display_indented<'s>(&'s self) -> impl fmt::Display + 's {
        writer::Display {
            inline: writer::Inline::Soft,
            clear_left: false,
            indentation: 0,
            increment: 2,
            node: self.0.borrow(),
        }
    }

    pub fn display_unindented<'s>(&'s self) -> impl fmt::Display + 's {
        writer::Display {
            inline: writer::Inline::Hard,
            clear_left: false,
            indentation: 0,
            increment: 0,
            node: self.0.borrow(),
        }
    }
}

impl Eq for PropList {}

impl PartialEq for PropList {
    fn eq(&self, other: &Self) -> bool {
        *self.0.borrow() == *other.0.borrow()
    }
}

impl hash::Hash for PropList {
    fn hash<H: hash::Hasher>(&self, h: &mut H) {
        self.0.borrow().hash(h)
    }
}

impl fmt::Debug for PropList {
    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
        write!(out, "{:?}", self.0.borrow())?;
        Ok(())
    }
}

impl PropList {
    pub fn deep_clone(&self) -> Self {
        match &*self.0.borrow() {
            Node::String(s) => PropList::new(Node::String(s.clone())),
            Node::Array(items) => {
                PropList::new(Node::Array(items.iter().map(|x| x.deep_clone()).collect()))
            }
            Node::Dictionary(items) => PropList::new(Node::Dictionary(
                items
                    .iter()
                    .map(|(k, v)| (k.deep_clone(), v.deep_clone()))
                    .collect(),
            )),
        }
    }

    /// Atomically serialize this `PropList` to `path`, creating any necessary
    /// parent directories.
    ///
    /// `path` is written to atomically: either the serialized `PropList` will
    /// be completely written to `path`, or the operation will fail and any
    /// existing file at `path` will not be modified.
    ///
    /// ## Rust rewrite notes
    ///
    /// As originally noted in `proplist.c`, a Coverity security bug report
    /// flagged the need to preserve the permissions on the file being written
    /// to. This should be respected in the rewritten code under Unix-like
    /// operataing systems.
    pub fn write_to_file<P: AsRef<Path>>(&self, path: &P) -> io::Result<()> {
        self.write_to_file_impl(path.as_ref())
    }

    fn write_to_file_impl(&self, path: &Path) -> io::Result<()> {
        if let Some(parent) = path.parent() {
            find_file::create_path_hierarchy(parent)?;
        }
        let file = atomic_write_file::AtomicWriteFile::options()
            .preserve_mode(true)
            .open(&path)?;
        let mut out = BufWriter::new(file);
        writeln!(&mut out, "{}", self.display_indented())?;
        out.into_inner()?.commit()
    }
}

pub mod ffi {
    use crate::{find_file::path_from_cstr, memory};

    use super::{
        merge_deep, merge_shallow, parser, subtract_deep, subtract_shallow, Node, PropList,
    };

    use std::{
        collections::HashMap, ffi::{c_char, c_int, c_uint, CStr, CString, OsString}, ptr, str::FromStr
    };

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePLString(s: *const c_char) -> *mut PropList {
        if s.is_null() {
            return ptr::null_mut();
        }
        let s = unsafe { CStr::from_ptr(s) };
        Box::leak(Box::new(PropList::new(Node::String(s.into()))))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePLArrayFromSlice(
        elems: *mut PropList,
        length: c_uint,
    ) -> *mut PropList {
        if elems.is_null() {
            return ptr::null_mut();
        }
        let elems = unsafe { &*ptr::slice_from_raw_parts(elems, length as usize) };
        Box::leak(Box::new(PropList::new(Node::Array(
            elems.iter().cloned().collect(),
        ))))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateEmptyPLArray() -> *mut PropList {
        Box::leak(Box::new(PropList::new(Node::Array(Vec::new()))))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePLDictionary(
        key: *mut PropList,
        value: *mut PropList,
    ) -> *mut PropList {
        if key.is_null() || value.is_null() {
            return Box::leak(Box::new(PropList::new(Node::Dictionary(HashMap::new()))));
        }
        let key = unsafe { (*key).clone() };
        let value = unsafe { (*value).clone() };
        Box::leak(Box::new(PropList::new(Node::Dictionary(
            [(key, value)].into(),
        ))))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateEmptyPLDictionary() -> *mut PropList {
        Box::leak(Box::new(PropList::new(Node::Dictionary(HashMap::new()))))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRetainPropList(plist: *mut PropList) -> *mut PropList {
        if plist.is_null() {
            return ptr::null_mut();
        }
        unsafe { Box::leak(Box::new((*plist).clone())) }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMReleasePropList(plist: *mut PropList) {
        if plist.is_null() {
            return;
        }
        let _ = unsafe { ptr::read(plist) };
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMInsertInPLArray(
        plist: *mut PropList,
        index: c_int,
        item: *mut PropList,
    ) {
        if plist.is_null() || index < 0 || item.is_null() {
            return;
        }
        let plist = unsafe { &mut *plist };
        if let Node::Array(ref mut items) = *plist.0.borrow_mut() {
            let item = unsafe { (*item).clone() };
            items.insert(index as usize, item);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMAddToPLArray(plist: *mut PropList, item: *mut PropList) {
        if plist.is_null() || item.is_null() {
            return;
        }
        let plist = unsafe { &mut *plist };
        if let Node::Array(ref mut items) = *plist.0.borrow_mut() {
            let item = unsafe { (*item).clone() };
            items.push(item);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDeleteFromPLArray(plist: *mut PropList, index: c_int) {
        if plist.is_null() || index < 0 {
            return;
        }
        let plist = unsafe { &mut *plist };
        if let Node::Array(ref mut items) = *plist.0.borrow_mut() {
            items.remove(index as usize);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRemoveFromPLArray(plist: *mut PropList, item: *mut PropList) {
        if plist.is_null() || item.is_null() {
            return;
        }
        let plist = unsafe { &mut *plist };
        let item = unsafe { &*item };
        if let Node::Array(ref mut items) = *plist.0.borrow_mut() {
            if let Some((i, _)) = items.iter().enumerate().find(|(_, x)| *x == item) {
                items.remove(i);
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMPutInPLDictionary(
        plist: *mut PropList,
        key: *mut PropList,
        value: *mut PropList,
    ) {
        if plist.is_null() || key.is_null() || value.is_null() {
            return;
        }
        let plist = unsafe { &mut *plist };
        if let Node::Dictionary(ref mut items) = *plist.0.borrow_mut() {
            let key = unsafe { (*key).clone() };
            let value = unsafe { (*value).clone() };
            items.insert(key, value);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRemoveFromPLDictionary(plist: *mut PropList, key: *mut PropList) {
        if plist.is_null() || key.is_null() {
            return;
        }
        let plist = unsafe { &mut *plist };
        let key = unsafe { &*key };
        if let Node::Dictionary(ref mut items) = *plist.0.borrow_mut() {
            items.remove(key);
        }
    }

    /// If `dest` and `source` are both dictionaries, overwrites entries in
    /// `dest` with corresponding entries in `source`.
    ///
    /// If `recursive` is non-zero, this is done recursively for values in
    /// `dest` and `source` that are both dictionaries.
    ///
    /// ## Rust rewrite notes
    ///
    /// This operation is used a few times. It may be worth keeping around
    /// longer-term, although it might be hard to express if we do transition
    /// away from `PropList`s to statically typed struct trees.
    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMMergePLDictionaries(
        dest: *mut PropList,
        source: *mut PropList,
        recursive: c_int,
    ) -> *mut PropList {
        if dest.is_null() || ptr::eq(dest, source) || source.is_null() {
            return dest;
        }

        let dest = unsafe { (*dest).clone() };
        let source = unsafe { (*source).clone() };

        if recursive == 0 {
            merge_shallow(dest.clone(), source);
        } else {
            merge_deep(dest.clone(), source);
        }

        return Box::leak(Box::new(dest));
    }

    /// If `dest` and `source` are both dictionaries, removes from `dest` any
    /// `(k, v)` pairs where `dest[k] == source[k]`.
    ///
    /// If `recursive` is non-zero, this is done recursively over subtrees of
    /// `dest` and `source` when both `dest` and `source` are dictionaries for
    /// keys of `source` that are also keys of `dest`.
    ///
    /// ## Rust rewrite notes
    ///
    /// This operation is only used in one place. It may be better to implement
    /// this behavior as a one-off closer to where it is used, or with a
    /// different API differently (e.g., as a function of a more general
    /// proplist diff).
    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSubtractPLDictionaries(
        dest: *mut PropList,
        source: *mut PropList,
        recursive: c_int,
    ) -> *mut PropList {
        if dest.is_null() {
            return ptr::null_mut();
        }
        if source.is_null() {
            return dest;
        }

        let dest = unsafe { (*dest).clone() };
        let source = unsafe { (*source).clone() };

        if recursive == 0 {
            subtract_shallow(dest.clone(), source);
        } else {
            subtract_deep(dest.clone(), source);
        }

        Box::leak(Box::new(dest))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPropListItemCount(plist: *mut PropList) -> c_int {
        if plist.is_null() {
            return 0;
        }
        let plist = unsafe { &*plist };
        match &*plist.0.borrow() {
            Node::Array(xs) => xs.len() as c_int,
            Node::Dictionary(xs) => xs.len() as c_int,
            _ => 0,
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMIsPLString(plist: *mut PropList) -> c_int {
        if plist.is_null() {
            return 0;
        }
        let plist = unsafe { &*plist };
        match &*plist.0.borrow() {
            Node::String(_) => 1,
            _ => 0,
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMIsPLArray(plist: *mut PropList) -> c_int {
        if plist.is_null() {
            return 0;
        }
        let plist = unsafe { &*plist };
        match &*plist.0.borrow() {
            Node::Array(_) => 1,
            _ => 0,
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMIsPLDictionary(plist: *mut PropList) -> c_int {
        if plist.is_null() {
            return 0;
        }
        let plist = unsafe { &*plist };
        match &*plist.0.borrow() {
            Node::Dictionary(_) => 1,
            _ => 0,
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMIsPropListEqualTo(a: *mut PropList, b: *mut PropList) -> c_int {
        if ptr::eq(a, b) {
            return 1;
        }
        if a.is_null() {
            return 0;
        }
        let a = unsafe { &*a };
        let b = unsafe { &*b };
        if a == b {
            1
        } else {
            0
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFromPLString(plist: *mut PropList) -> *const c_char {
        if plist.is_null() {
            return ptr::null_mut();
        }
        let plist = unsafe { &*plist };
        if let Node::String(ref s) = *plist.0.borrow() {
            s.as_ref().as_ptr().cast::<c_char>()
        } else {
            ptr::null()
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFromPLArray(plist: *mut PropList, index: c_int) -> *mut PropList {
        if plist.is_null() || index < 0 {
            return ptr::null_mut();
        }
        let plist = unsafe { &*plist };
        if let Node::Array(ref items) = *plist.0.borrow() {
            if let Some(x) = items.get(index as usize) {
                return Box::leak(Box::new(x.clone()));
            }
        }
        ptr::null_mut()
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFromPLDictionary(plist: *mut PropList, key: *mut PropList) -> *mut PropList {
        if plist.is_null() || key.is_null() {
            return ptr::null_mut();
        }
        let plist = unsafe { &*plist };
        let key = unsafe { &*key };
        if let Node::Dictionary(ref items) = *plist.0.borrow() {
            if let Some(item) = items.get(key) {
                return Box::leak(Box::new(item.clone()));
            }
        }
        ptr::null_mut()
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPLDictionaryKeys(plist: *mut PropList) -> *mut PropList {
        if plist.is_null() {
            return ptr::null_mut();
        }
        let plist = unsafe { &*plist };

        if let Node::Dictionary(ref items) = *plist.0.borrow() {
            return Box::leak(Box::new(PropList::new(Node::Array(items.keys().cloned().collect()))));
        }
        ptr::null_mut()
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDeepCopyPropList(plist: *mut PropList) -> *mut PropList {
        if plist.is_null() {
            return ptr::null_mut();
        }
        let plist = unsafe { &*plist };
        Box::leak(Box::new(plist.deep_clone()))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePropListFromDescription(desc: *const c_char) -> *mut PropList {
        if desc.is_null() {
            return ptr::null_mut();
        }
        let desc = unsafe { CStr::from_ptr(desc) };
        let Ok(desc) = desc.to_str() else {
            return ptr::null_mut();
        };

        match parser::from_str(desc) {
            Ok(plist) => Box::leak(Box::new(plist)),
            Err(_) => ptr::null_mut(),
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPropListDescription(
        plist: *mut PropList,
        indented: c_int,
    ) -> *mut c_char {
        use std::io::Write;

        if plist.is_null() {
            return ptr::null_mut();
        }
        let plist = unsafe { &*plist };
        let mut buf = Vec::new();
        if indented != 0 {
            if let Err(_) = write!(&mut buf, "{}", plist.display_indented()) {
                return ptr::null_mut();
            }
        } else {
            if let Err(_) = write!(&mut buf, "{}", plist.display_unindented()) {
                return ptr::null_mut();
            }
        }
        match CString::new(buf) {
            Ok(s) => memory::alloc_string(s.as_c_str()),
            Err(_) => ptr::null_mut(),
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMReadPropListFromFile(path: *const c_char) -> *mut PropList {
        if path.is_null() {
            return ptr::null_mut();
        }
        let path = unsafe { CStr::from_ptr(path) };
        let Ok(path) = path.to_str() else {
            return ptr::null_mut();
        };
        match PropList::from_file(path) {
            Ok(plist) => Box::leak(Box::new(plist)),
            Err(_) => {
                // TODO: print error message.
                ptr::null_mut()
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMReadPropListFromPipe(command: *const c_char) -> *mut PropList {
        if command.is_null() {
            return ptr::null_mut();
        }

        let command = unsafe { CStr::from_ptr(command) };
        let Ok(command) = command.to_str() else {
            return ptr::null_mut();
        };
        let command = OsString::from_str(command).unwrap();

        let Ok(output) = std::process::Command::new("/bin/sh")
            .arg("-c")
            .arg(command)
            .output()
        else {
            // TODO: print error message.
            return ptr::null_mut();
        };
        let Ok(output) = String::from_utf8(output.stdout) else {
            // TODO: print error message.
            return ptr::null_mut();
        };
        match parser::from_str(&output) {
            Ok(plist) => Box::leak(Box::new(plist)),
            Err(_) => {
                // TODO: print error message.
                ptr::null_mut()
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMWritePropListToFile(
        plist: *mut PropList,
        path: *const c_char,
    ) -> c_int {
        if plist.is_null() || path.is_null() {
            return 0;
        }
        let plist = unsafe {
            &*plist
        };

        let path = unsafe {
            CStr::from_ptr(path)
        };
        let Some(path) = path_from_cstr(path) else {
            // TODO: complain.
            return 0;
        };

        match plist.write_to_file(&path) {
            Ok(_) => return 1,
            Err(_) => {
                // TODO: complain.
                return 0;
            }
        }
    }
}

#[cfg(test)]
mod test {
    use std::ffi::CString;

    use crate::memory;

    use super::{Node, PropList, ffi};

    #[test]
    fn free_proplist_description() {
        let mut plist = PropList::new(Node::Array(vec![PropList::new(Node::String(CString::from(c"hello"))),
                                                   PropList::new(Node::String(CString::from(c"world!")))]));
        let desc = unsafe { ffi::WMGetPropListDescription(&mut plist, 1) };
        assert!(!desc.is_null());

        unsafe { memory::ffi::wfree(desc.cast()); }
    }

    #[test]
    fn oob_array_access_returns_null() {
        // This is the original WMArray behavior. I don't like it, but a bunch
        // of existing code relies on it.
        let mut list = PropList::new(Node::Array(vec![PropList::new(Node::String(CString::from(c"hello"))),
                                                      PropList::new(Node::String(CString::from(c"world!")))]));
        assert!(unsafe { ffi::WMGetFromPLArray(&mut list, 3) }.is_null());
    }
}
