cbor-cli 0.7.0

Encode, decode, and inspect CBOR (RFC 8949) from the command line. Convert between CBOR, JSON, YAML, and TOML via serde, with streaming support for CBOR sequences.
Documentation
use std::io;

use cbor2::de::Deserializer;
use cbor2::value::Value as CborValue;
use anyhow::Context;

/// Convert a stream of CBOR bytes into a stream of some other format.
///
/// Each top-level CBOR item is decoded and re-serialized to `format`, with
/// `delimiter` written between items. The input is streamed one item at a
/// time and never buffered.
///
/// # Arguments
///
/// - `format`: one of `"json"`, `"yaml"`, `"toml"`, `"cbor"`.
/// - `delimiter`: bytes written between consecutive items (e.g. `"\n"`). An
///   empty input produces no output and no delimiter.
/// - `pretty`: if true and `format` is `"json"`, use pretty-printed output.
/// - `reader`: the CBOR input stream. Supports CBOR sequences (RFC 8742).
/// - `writer`: the output sink.
///
/// # Example
///
/// ```
/// use cbor_cli::export::export_from_reader;
///
/// // CBOR for the integer 42.
/// let cbor = [0x18, 0x2a];
/// let mut json = Vec::new();
/// export_from_reader("json", "\n", false, &cbor[..], &mut json).unwrap();
/// assert_eq!(json, b"42");
/// ```
pub fn export_from_reader<R: io::Read, W: io::Write>(
  format: &str,
  delimiter: &str,
  pretty: bool,
  reader: R,
  writer: &mut W,
) -> anyhow::Result<()> {
  let mut first = true;
  for v in Deserializer::from_reader(reader).into_iter::<CborValue>() {
    let v = v?;
    if !first {
      writer.write_all(delimiter.as_bytes())?;
    }
    first = false;

    if format == "json" {
      if pretty {
        serde_json::to_writer_pretty(&mut *writer, &v).context("failed to serialize JSON")?;
      } else {
        serde_json::to_writer(&mut *writer, &v).context("failed to serialize JSON")?;
      }
    } else if format == "yaml" {
      serde_yaml::to_writer(&mut *writer, &v).context("failed to serialize YAML")?;
    } else if format == "toml" {
      let s = toml::ser::to_string(&v).context("failed to serialize TOML")?;
      writer.write_all(s.as_bytes())?;
    } else {
      cbor2::to_writer(&v, &mut *writer).context("failed to serialize CBOR")?;
    }
  }
  Ok(())
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_export_array() {
    let cbor = [0x82, 0x01, 0x02];
    let cases: [(&str, bool, &[u8]); 2] = [
      ("json", false, b"[1,2]"),
      ("yaml", false, b"- 1\n- 2\n"),
    ];
    for (format, pretty, expected) in cases {
      let mut output = Vec::new();
      export_from_reader(format, "\n", pretty, &cbor[..], &mut output).unwrap();
      assert_eq!(output, expected, "format: {format}");
    }
  }

  #[test]
  fn test_export_cbor() {
    let input = [0x82, 0x01, 0x02];
    let mut output = Vec::new();
    export_from_reader("cbor", "\n", false, &input[..], &mut output).unwrap();
    assert_eq!(output, b"\x82\x01\x02" as &[u8]);
  }

  #[test]
  fn test_export_sequence() {
    // Two CBOR items: map {"a":"a"} then string "a"
    let input = [0xa1, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61];
    let mut output = Vec::new();
    export_from_reader("json", "\n", false, &input[..], &mut output).unwrap();
    assert_eq!(output, b"{\"a\":\"a\"}\n\"a\"" as &[u8]);
  }

  #[test]
  fn test_export_pretty_json() {
    let cbor = [0x82, 0x01, 0x02];
    let mut compact = Vec::new();
    export_from_reader("json", "\n", false, &cbor[..], &mut compact).unwrap();

    let mut pretty = Vec::new();
    export_from_reader("json", "\n", true, &cbor[..], &mut pretty).unwrap();

    assert_eq!(compact, b"[1,2]");
    assert_ne!(pretty, compact);
    assert!(pretty.windows(2).any(|w| w == b"\n "));
  }
}