univert 0.4.0

Universal file converter (library and CLI)
Documentation
//! CBOR (Concise Binary Object Representation, RFC 8949) via ciborium.

use std::fs;
use std::io::{BufReader, BufWriter};

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

pub fn cbor_to_json(job: &Job) -> Result<()> {
  let file = fs::File::open(job.input)?;
  let value: ciborium::value::Value =
    ciborium::de::from_reader(BufReader::new(file))
      .map_err(|e| Error::other(format!("cbor decode: {e}")))?;
  let json = cbor_to_serde(value)?;
  let out = serde_json::to_string_pretty(&json)
    .map_err(|e| Error::other(format!("json serialize: {e}")))?;
  fs::write(job.output, out + "\n")?;
  Ok(())
}

pub fn json_to_cbor(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 cbor = serde_to_cbor(value);
  let file = fs::File::create(job.output)?;
  ciborium::ser::into_writer(&cbor, BufWriter::new(file))
    .map_err(|e| Error::other(format!("cbor encode: {e}")))?;
  Ok(())
}

fn cbor_to_serde(value: ciborium::value::Value) -> Result<serde_json::Value> {
  use ciborium::value::Value as C;
  use serde_json::Value as J;
  Ok(match value {
    C::Null => J::Null,
    C::Bool(b) => J::Bool(b),
    C::Integer(i) => {
      let n: i128 = i.into();
      if let Ok(v) = i64::try_from(n) {
        J::Number(v.into())
      } else if let Ok(v) = u64::try_from(n) {
        J::Number(v.into())
      } else {
        J::String(n.to_string())
      }
    }
    C::Float(f) => serde_json::Number::from_f64(f)
      .map(J::Number)
      .unwrap_or(J::Null),
    C::Text(s) => J::String(s),
    C::Bytes(b) => {
      let mut hex = String::with_capacity(b.len() * 2);
      for byte in b {
        let _ =
          std::fmt::Write::write_fmt(&mut hex, format_args!("{byte:02x}"));
      }
      J::String(hex)
    }
    C::Array(items) => J::Array(
      items
        .into_iter()
        .map(cbor_to_serde)
        .collect::<Result<Vec<_>>>()?,
    ),
    C::Map(pairs) => {
      let mut map = serde_json::Map::with_capacity(pairs.len());
      for (k, v) in pairs {
        let key = match k {
          C::Text(s) => s,
          other => serde_json::to_string(&cbor_to_serde(other)?)
            .map_err(|e| Error::other(format!("cbor key: {e}")))?,
        };
        map.insert(key, cbor_to_serde(v)?);
      }
      J::Object(map)
    }
    C::Tag(_, inner) => cbor_to_serde(*inner)?,
    _ => J::Null,
  })
}

fn serde_to_cbor(value: serde_json::Value) -> ciborium::value::Value {
  use ciborium::value::Value as C;
  use serde_json::Value as J;
  match value {
    J::Null => C::Null,
    J::Bool(b) => C::Bool(b),
    J::Number(n) => {
      if let Some(i) = n.as_i64() {
        C::Integer(i.into())
      } else if let Some(u) = n.as_u64() {
        C::Integer(u.into())
      } else {
        C::Float(n.as_f64().unwrap_or(0.0))
      }
    }
    J::String(s) => C::Text(s),
    J::Array(items) => C::Array(items.into_iter().map(serde_to_cbor).collect()),
    J::Object(map) => C::Map(
      map
        .into_iter()
        .map(|(k, v)| (C::Text(k), serde_to_cbor(v)))
        .collect(),
    ),
  }
}