Skip to content

Commit

Permalink
feat(cli): add diag2* commands, minor diag fixes, README update
Browse files Browse the repository at this point in the history
Can now parse diagnostic output as input, to produce CLI commands:
* diag2bin
* diag2hex
* diag2json

Minor fix for diagnostic output when printing length-prefix of recursives,
strings and bytes.

Updated README to accurately demonstrate all of the CLI features.
  • Loading branch information
rvagg committed Feb 21, 2022
1 parent 089d9a7 commit bb52262
Show file tree
Hide file tree
Showing 4 changed files with 422 additions and 181 deletions.
118 changes: 107 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@

* [Example](#example)
* [CLI](#cli)
* [`cborg json2hex '<json string>'`](#cborg-json2hex-json-string)
* [`cborg hex2json [--pretty] <hex string>`](#cborg-hex2json---pretty-hex-string)
* [`cborg hex2diag <hex string>`](#cborg-hex2diag-hex-string)
* [`cborg bin2diag [binary input]`](#cborg-bin2diag-binary-input)
* [`cborg bin2hex [binary string]`](#cborg-bin2hex-binary-string)
* [`cborg bin2json [--pretty] [binary input]`](#cborg-bin2json---pretty-binary-input)
* [`cborg diag2bin [diagnostic string]`](#cborg-diag2bin-diagnostic-string)
* [`cborg diag2hex [diagnostic string]`](#cborg-diag2hex-diagnostic-string)
* [`cborg diag2json [--pretty] [diagnostic string]`](#cborg-diag2json---pretty-diagnostic-string)
* [`cborg hex2bin [hex string]`](#cborg-hex2bin-hex-string)
* [`cborg hex2diag [hex string]`](#cborg-hex2diag-hex-string)
* [`cborg hex2json [--pretty] [hex string]`](#cborg-hex2json---pretty-hex-string)
* [`cborg json2bin [json string]`](#cborg-json2bin-json-string)
* [`cborg json2diag [json string]`](#cborg-json2diag-json-string)
* [`cborg json2hex '[json string]'`](#cborg-json2hex-json-string)
* [API](#api)
* [`encode(object[, options])`](#encodeobject-options)
* [Options](#options)
Expand Down Expand Up @@ -50,18 +59,87 @@ encoded: Uint8Array(21) [

When installed globally via `npm` (with `npm install cborg --global`), the `cborg` command will be available that provides some handy CBOR CLI utilities. Run with `cborg help` for additional details.

### `cborg json2hex '<json string>'`
The following commands take either input from the command line, or if no input is supplied will read from stdin. Output is printed to stdout. So you can `cat foo | cborg <command>`.

Convert a JSON object into CBOR bytes in hexadecimal format.
### `cborg bin2diag [binary input]`

Convert CBOR from binary input to a CBOR diagnostic output format which explains the byte contents.

```
$ cborg json2hex '["a", "b", 1, "😀"]'
$ cborg hex2bin 84616161620164f09f9880 | cborg bin2diag
84 # array(4)
61 # string(1)
61 # "a"
61 # string(1)
62 # "b"
01 # uint(1)
64 f09f # string(2)
f09f9880 # "😀"
```

### `cborg bin2hex [binary string]`

A utility method to convert a binary input (stdin only) to hexadecimal output (does not involve CBOR).

### `cborg bin2json [--pretty] [binary input]`

Convert CBOR from binary input to JSON format.

```
$ cborg hex2bin 84616161620164f09f9880 | cborg bin2json
["a","b",1,"😀"]
```

### `cborg diag2bin [diagnostic string]`

Convert a CBOR diagnostic string to a binary data form of the CBOR.

```
$ cborg json2diag '["a","b",1,"😀"]' | cborg diag2bin | cborg bin2hex
84616161620164f09f9880
```

### `cborg hex2json [--pretty] <hex string>`
### `cborg diag2hex [diagnostic string]`

Convert a hexadecimal string to a JSON format.
Convert a CBOR diagnostic string to the CBOR bytes in hexadecimal format.

```
$ cborg json2diag '["a","b",1,"😀"]' | cborg diag2hex
84616161620164f09f9880
```

### `cborg diag2json [--pretty] [diagnostic string]`

Convert a CBOR diagnostic string to JSON format.

```
$ cborg json2diag '["a","b",1,"😀"]' | cborg diag2json
["a","b",1,"😀"]
```

### `cborg hex2bin [hex string]`

A utility method to convert a hex string to binary output (does not involve CBOR).

### `cborg hex2diag [hex string]`

Convert CBOR from a hexadecimal string to a CBOR diagnostic output format which explains the byte contents.

```
$ cborg hex2diag 84616161620164f09f9880
84 # array(4)
61 # string(1)
61 # "a"
61 # string(1)
62 # "b"
01 # uint(1)
64 f09f # string(2)
f09f9880 # "😀"
```

### `cborg hex2json [--pretty] [hex string]`

Convert CBOR from a hexadecimal string to JSON format.

```
$ cborg hex2json 84616161620164f09f9880
Expand All @@ -75,12 +153,21 @@ $ cborg hex2json --pretty 84616161620164f09f9880
]
```

### `cborg hex2diag <hex string>`
### `cborg json2bin [json string]`

Convert a hexadecimal string to a CBOR diagnostic output format which explains the byte contents.
Convert a JSON object into a binary data form of the CBOR.

```
$ cborg hex2diag 84616161620164f09f9880
$ cborg json2bin '["a","b",1,"😀"]' | cborg bin2hex
84616161620164f09f9880
```

### `cborg json2diag [json string]`

Convert a JSON object into a CBOR diagnostic output format which explains the contents of the CBOR form of the input object.

```
$ cborg json2diag '["a", "b", 1, "😀"]'
84 # array(4)
61 # string(1)
61 # "a"
Expand All @@ -91,6 +178,15 @@ $ cborg hex2diag 84616161620164f09f9880
f09f9880 # "😀"
```

### `cborg json2hex '[json string]'`

Convert a JSON object into CBOR bytes in hexadecimal format.

```
$ cborg json2hex '["a", "b", 1, "😀"]'
84616161620164f09f9880
```

## API

### `encode(object[, options])`
Expand Down
79 changes: 52 additions & 27 deletions lib/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import process from 'process'
import { decode, encode } from '../cborg.js'
import { tokensToDiagnostic } from './diagnostic.js'
import { tokensToDiagnostic, fromDiag } from './diagnostic.js'
import { fromHex as _fromHex, toHex } from './byte-utils.js'

/**
Expand All @@ -11,15 +11,18 @@ import { fromHex as _fromHex, toHex } from './byte-utils.js'
function usage (code) {
console.error('Usage: cborg <command> <args>')
console.error('Valid commands:')
console.error('\thex2diag [hex input]')
console.error('\thex2bin [hex input]')
console.error('\thex2json [--pretty] [hex input]')
console.error('\tbin2hex [binary input]')
console.error('\tbin2diag [binary input]')
console.error('\tbin2hex [binary input]')
console.error('\tbin2json [--pretty] [binary input]')
console.error('\tjson2hex \'[json input]\'')
console.error('\tjson2diag \'[json input]\'')
console.error('\tdiag2bin [diagnostic input]')
console.error('\tdiag2hex [diagnostic input]')
console.error('\tdiag2json [--pretty] [diagnostic input]')
console.error('\thex2bin [hex input]')
console.error('\thex2diag [hex input]')
console.error('\thex2json [--pretty] [hex input]')
console.error('\tjson2bin \'[json input]\'')
console.error('\tjson2diag \'[json input]\'')
console.error('\tjson2hex \'[json input]\'')
console.error('Input may either be supplied as an argument or piped via stdin')
process.exit(code || 0)
}
Expand Down Expand Up @@ -59,26 +62,15 @@ async function run () {
return usage(0)
}

case 'hex2json': {
const { argv, pretty } = argvPretty()
const bin = fromHex(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}

case 'hex2diag': {
const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
case 'bin2diag': {
/* c8 ignore next 1 */
const bin = process.argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(process.argv[3])
for (const line of tokensToDiagnostic(bin)) {
console.log(line)
}
return
}

case 'hex2bin': {
// this is really nothing to do with cbor.. just handy
const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return process.stdout.write(bin)
}

case 'bin2hex': {
// this is really nothing to do with cbor.. just handy
/* c8 ignore next 1 */
Expand All @@ -93,19 +85,52 @@ async function run () {
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}

case 'bin2diag': {
case 'diag2bin': {
// no coverage on windows for non-stdin input
/* c8 ignore next 1 */
const bin = process.argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(process.argv[3])
const bin = fromDiag(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return process.stdout.write(bin)
}

case 'diag2hex': {
// no coverage on windows for non-stdin input
/* c8 ignore next 1 */
const bin = fromDiag(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return console.log(toHex(bin))
}

case 'diag2json': {
const { argv, pretty } = argvPretty()
// no coverage on windows for non-stdin input
/* c8 ignore next 1 */
const bin = fromDiag(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}

case 'hex2bin': {
// this is really nothing to do with cbor.. just handy
const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return process.stdout.write(bin)
}

case 'hex2diag': {
const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
for (const line of tokensToDiagnostic(bin)) {
console.log(line)
}
return
}

case 'json2hex': {
case 'hex2json': {
const { argv, pretty } = argvPretty()
const bin = fromHex(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}

case 'json2bin': {
const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
const obj = JSON.parse(inp)
return console.log(toHex(encode(obj)))
return process.stdout.write(encode(obj))
}

case 'json2diag': {
Expand All @@ -117,10 +142,10 @@ async function run () {
return
}

case 'json2bin': {
case 'json2hex': {
const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
const obj = JSON.parse(inp)
return process.stdout.write(encode(obj))
return console.log(toHex(encode(obj)))
}

default: { // no, or unknown cmd
Expand Down
59 changes: 54 additions & 5 deletions lib/diagnostic.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Tokeniser } from './decode.js'
import { toHex } from './byte-utils.js'
import { toHex, fromHex } from './byte-utils.js'
import { uintBoundaries } from './0uint.js'

const utf8Encoder = new TextEncoder()
const utf8Decoder = new TextDecoder()

/**
* @param {Uint8Array} inp
Expand All @@ -26,6 +30,7 @@ function * tokensToDiagnostic (inp, width = 100) {
let vLength = token.encodedLength - 1
/** @type {string|number} */
let v = String(token.value)
let outp = `${margin}${slc(0, 1)}`
const str = token.type.name === 'bytes' || token.type.name === 'string'
if (token.type.name === 'string') {
v = v.length
Expand All @@ -36,7 +41,33 @@ function * tokensToDiagnostic (inp, width = 100) {
vLength -= v
}

let outp = `${margin}${slc(0, 1)} ${slc(1, vLength)}`
let multilen
switch (token.type.name) {
case 'string':
case 'bytes':
case 'map':
case 'array':
// for bytes and string, we want to print out the length part of the value prefix if it
// exists - it exists for short lengths (<24) but does for longer lengths
multilen = token.type.name === 'string' ? utf8Encoder.encode(token.value).length : token.value.length
if (multilen >= uintBoundaries[0]) {
if (multilen < uintBoundaries[1]) {
outp += ` ${slc(1, 1)}`
} else if (multilen < uintBoundaries[2]) {
outp += ` ${slc(1, 2)}`
/* c8 ignore next 5 */
} else if (multilen < uintBoundaries[3]) { // sus
outp += ` ${slc(1, 4)}`
} else if (multilen < uintBoundaries[4]) { // orly?
outp += ` ${slc(1, 8)}`
}
}
break
default:
// print the value if it's not compacted into the first byte
outp += ` ${slc(1, vLength)}`
break
}

outp = outp.padEnd(width / 2, ' ')
outp += `# ${margin}${token.type.name}`
Expand All @@ -47,7 +78,7 @@ function * tokensToDiagnostic (inp, width = 100) {

if (str) {
margin += ' '
const repr = token.type.name === 'bytes' ? token.value : new TextEncoder().encode(token.value)
const repr = token.type.name === 'bytes' ? token.value : utf8Encoder.encode(token.value)
const wh = ((width / 2) - margin.length - 1) / 2
let snip = 0
while (repr.length - snip > 0) {
Expand All @@ -56,7 +87,7 @@ function * tokensToDiagnostic (inp, width = 100) {
// the assumption that we can utf8 a byte-sliced version is a stretch,
// we could be slicing in the middle of a multi-byte character
const st = token.type.name === 'string'
? new TextDecoder().decode(piece)
? utf8Decoder.decode(piece)
: piece.reduce((/** @type {string} */ p, /** @type {number} */ c) => {
if (c < 0x20 || c === 0x7f) {
return `${p}\\x${c.toString(16).padStart(2, '0')}`
Expand Down Expand Up @@ -95,4 +126,22 @@ function * tokensToDiagnostic (inp, width = 100) {
}
}

export { tokensToDiagnostic }
/**
* Convert an input string formatted as CBOR diagnostic output into binary CBOR form.
* @param {string} input
* @returns {Uint8Array}
*/
function fromDiag (input) {
/* c8 ignore next 3 */
if (typeof input !== 'string') {
throw new TypeError('Expected string input')
}
input = input.replace(/#.*?$/mg, '').replace(/[\s\r\n]+/mg, '')
/* c8 ignore next 3 */
if (/[^a-f0-9]/i.test(input)) {
throw new TypeError('Input string was not CBOR diagnostic format')
}
return fromHex(input)
}

export { tokensToDiagnostic, fromDiag }
Loading

0 comments on commit bb52262

Please sign in to comment.