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::fs;

use assert_cmd::Command;
use tempfile::tempdir;

fn cbor() -> Command {
  Command::cargo_bin("cbor").unwrap()
}

fn write_fixture(dir: &std::path::Path, name: &str, content: &[u8]) {
  fs::write(dir.join(name), content).unwrap();
}

// --- Import + Export roundtrips ---

#[test]
fn json_roundtrip() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "in.json", b"{\"a\":1,\"b\":[2,3]}");

  let cbor_bytes = cbor()
    .arg("import")
    .arg(dir.path().join("in.json"))
    .assert()
    .success()
    .get_output()
    .stdout
    .clone();

  let mut cmd = cbor();
  cmd.arg("export").arg("--format=json").write_stdin(cbor_bytes);
  cmd.assert().success().stdout("{\"a\":1,\"b\":[2,3]}");
}

#[test]
fn yaml_roundtrip() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "in.yaml", b"a: 1\nb:\n- 2\n- 3\n");

  let cbor_bytes = cbor()
    .arg("import")
    .arg(dir.path().join("in.yaml"))
    .assert()
    .success()
    .get_output()
    .stdout
    .clone();

  let mut cmd = cbor();
  cmd.arg("export").arg("--format=yaml").write_stdin(cbor_bytes);
  cmd.assert().success().stdout("a: 1\nb:\n- 2\n- 3\n");
}

#[test]
fn toml_roundtrip() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "in.toml", b"a = 1\nb = [2, 3]\n");

  let cbor_bytes = cbor()
    .arg("import")
    .arg(dir.path().join("in.toml"))
    .assert()
    .success()
    .get_output()
    .stdout
    .clone();

  let mut cmd = cbor();
  cmd.arg("export").arg("--format=toml").write_stdin(cbor_bytes);
  cmd.assert().success().stdout("a = 1\nb = [2, 3]\n");
}

// --- Stdin ---

#[test]
fn stdin_import_export() {
  let cbor_bytes = cbor()
    .arg("import")
    .arg("--format=json")
    .write_stdin("{\"x\":1}")
    .assert()
    .success()
    .get_output()
    .stdout
    .clone();

  let mut export_cmd = cbor();
  export_cmd
    .arg("export")
    .arg("--format=json")
    .write_stdin(cbor_bytes);
  export_cmd.assert().success().stdout("{\"x\":1}");
}

// --- Stdin import without --format errors ---

#[test]
fn stdin_import_without_format_errors() {
  let mut cmd = cbor();
  cmd.arg("import").write_stdin("{\"x\":1}");
  cmd.assert().failure().stderr(predicates::str::contains("No format specified"));
}

// --- Multi-file ---

#[test]
fn multi_file_import() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "a.json", b"1");
  write_fixture(dir.path(), "b.json", b"2");

  let cbor_bytes = cbor()
    .arg("import")
    .arg("--format=json")
    .arg(dir.path().join("a.json"))
    .arg(dir.path().join("b.json"))
    .assert()
    .success()
    .get_output()
    .stdout
    .clone();

  // Two CBOR integers concatenated
  assert_eq!(&cbor_bytes, b"\x01\x02");

  let mut cmd = cbor();
  cmd.arg("export").arg("--format=json").write_stdin(cbor_bytes);
  cmd.assert().success().stdout("1\n2");
}

// --- Custom delimiter ---

#[test]
fn custom_delimiter() {
  let data = b"\x01\x02"; // two integers: 1 and 2

  let mut cmd = cbor();
  cmd
    .arg("-d=,")
    .arg("export")
    .arg("--format=json")
    .write_stdin(&data[..]);
  cmd.assert().success().stdout("1,2");
}

// --- YAML default --- separator ---

#[test]
fn yaml_default_document_separator() {
  let data = b"\x01\x02"; // two integers: 1 and 2

  let mut cmd = cbor();
  cmd.arg("export").arg("--format=yaml").write_stdin(&data[..]);
  // YAML serializer emits trailing newlines, delimiter is "\n---\n"
  cmd.assert().success().stdout("1\n\n---\n2\n");
}

// --- Verbose output goes to stderr, not stdout ---

#[test]
fn verbose_goes_to_stderr() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "x.cbor", b"\x01");

  let assert = cbor()
    .arg("-vv")
    .arg("export")
    .arg("--format=json")
    .arg(dir.path().join("x.cbor"))
    .assert()
    .success();

  // stdout should only have the data
  assert.stdout("1");
}

// --- Missing file error ---

#[test]
fn missing_file_error() {
  let dir = tempdir().unwrap();
  let nonexistent = dir.path().join("nope.cbor");

  cbor()
    .arg("inspect")
    .arg(&nonexistent)
    .assert()
    .failure()
    .stderr(predicates::str::contains("File does not exist"));
}

// --- .yml extension ---

#[test]
fn yml_extension_supported() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "test.yml", b"[1, 2]\n");

  cbor()
    .arg("import")
    .arg(dir.path().join("test.yml"))
    .assert()
    .success();
}

// --- Inspect ---

#[test]
fn inspect_output() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "test.cbor", b"\x82\x01\x02");

  cbor()
    .arg("inspect")
    .arg(dir.path().join("test.cbor"))
    .assert()
    .success()
    .stdout(predicates::str::contains("[1, 2]"));
}

#[test]
fn inspect_stdin() {
  let mut cmd = cbor();
  cmd.arg("inspect").write_stdin(b"\x18\x2a"); // CBOR for 42
  cmd.assert().success().stdout("42\n");
}

// --- Contextual error messages ---

#[test]
fn invalid_cbor_error() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "bad.cbor", b"\xff");

  cbor()
    .arg("export")
    .arg("--format=json")
    .arg(dir.path().join("bad.cbor"))
    .assert()
    .failure()
    .stderr(predicates::str::contains("Failed to export"));
}

// --- Delimiter unescaping ---

#[test]
fn delimiter_unescape_newline() {
  let data = b"\x01\x02";

  let mut cmd = cbor();
  cmd
    .arg(r"-d=\n")
    .arg("export")
    .arg("--format=json")
    .write_stdin(&data[..]);

  let output = cmd.assert().success().get_output().stdout.clone();
  let output = String::from_utf8(output).unwrap();
  assert!(output.contains('\n'), "should contain a literal newline: {output:?}");
}

// --- Cross-format conversion chain ---

#[test]
fn cross_format_json_to_yaml() {
  let dir = tempdir().unwrap();
  write_fixture(dir.path(), "in.json", b"{\"key\": \"value\"}");

  // json -> cbor -> yaml
  let cbor_bytes = cbor()
    .arg("import")
    .arg(dir.path().join("in.json"))
    .assert()
    .success()
    .get_output()
    .stdout
    .clone();

  let mut cmd = cbor();
  cmd
    .arg("export")
    .arg("--format=yaml")
    .write_stdin(cbor_bytes);
  cmd.assert().success().stdout("key: value\n");
}