univert 0.4.0

Universal file converter (library and CLI)
Documentation
//! Apple property list (binary and XML variants).

use std::fs;
use std::io::BufReader;

use crate::{Error, Job, Result};

pub fn plist_to_json(job: &Job) -> Result<()> {
  let value = read_plist(job)?;
  let json = serde_json::to_string_pretty(&plist_to_serde(value))
    .map_err(|e| Error::other(format!("json serialize: {e}")))?;
  fs::write(job.output, json + "\n")?;
  Ok(())
}

pub fn json_to_plist(job: &Job) -> Result<()> {
  let raw = fs::read_to_string(job.input)?;
  let value: serde_json::Value = serde_json::from_str(&raw)
    .map_err(|e| Error::other(format!("invalid JSON: {e}")))?;
  let plist_value = serde_to_plist(value);
  plist_value
    .to_file_xml(job.output)
    .map_err(|e| Error::other(format!("plist write: {e}")))?;
  Ok(())
}

fn read_plist(job: &Job) -> Result<plist::Value> {
  let file = fs::File::open(job.input)?;
  plist::Value::from_reader(BufReader::new(file))
    .map_err(|e| Error::other(format!("plist read: {e}")))
}

fn plist_to_serde(value: plist::Value) -> serde_json::Value {
  use serde_json::Value as J;
  match value {
    plist::Value::Array(arr) => {
      J::Array(arr.into_iter().map(plist_to_serde).collect())
    }
    plist::Value::Dictionary(dict) => {
      let mut map = serde_json::Map::with_capacity(dict.len());
      for (k, v) in dict {
        map.insert(k, plist_to_serde(v));
      }
      J::Object(map)
    }
    plist::Value::Boolean(b) => J::Bool(b),
    plist::Value::Integer(i) => i
      .as_signed()
      .map(|v| J::Number(v.into()))
      .or_else(|| i.as_unsigned().map(|v| J::Number(v.into())))
      .unwrap_or(J::Null),
    plist::Value::Real(f) => serde_json::Number::from_f64(f)
      .map(J::Number)
      .unwrap_or(J::Null),
    plist::Value::String(s) => J::String(s),
    plist::Value::Date(d) => J::String(format!("{d:?}")),
    plist::Value::Data(bytes) => {
      let mut hex = String::with_capacity(bytes.len() * 2);
      for b in bytes {
        let _ = std::fmt::Write::write_fmt(&mut hex, format_args!("{b:02x}"));
      }
      J::String(hex)
    }
    plist::Value::Uid(uid) => J::Number(uid.get().into()),
    _ => J::Null,
  }
}

fn serde_to_plist(value: serde_json::Value) -> plist::Value {
  use serde_json::Value as J;
  match value {
    J::Null => plist::Value::String(String::new()),
    J::Bool(b) => plist::Value::Boolean(b),
    J::Number(n) => {
      if let Some(i) = n.as_i64() {
        plist::Value::Integer(i.into())
      } else if let Some(u) = n.as_u64() {
        plist::Value::Integer(u.into())
      } else {
        plist::Value::Real(n.as_f64().unwrap_or(0.0))
      }
    }
    J::String(s) => plist::Value::String(s),
    J::Array(items) => {
      plist::Value::Array(items.into_iter().map(serde_to_plist).collect())
    }
    J::Object(map) => {
      let mut dict = plist::Dictionary::new();
      for (k, v) in map {
        dict.insert(k, serde_to_plist(v));
      }
      plist::Value::Dictionary(dict)
    }
  }
}