diff --git a/lib/bin.js b/lib/bin.js index 50081e2..b8ec7a7 100755 --- a/lib/bin.js +++ b/lib/bin.js @@ -3,9 +3,7 @@ import process from 'process' import { decode, encode } from '../cborg.js' import { tokensToDiagnostic } from './diagnostic.js' -import { fromHex, toHex } from './byte-utils.js' - -const cmd = process.argv[2] +import { fromHex as _fromHex, toHex } from './byte-utils.js' /** * @param {number} code @@ -13,50 +11,137 @@ const cmd = process.argv[2] function usage (code) { console.error('Usage: cborg ') console.error('Valid commands:') - console.error('\thex2diag ') - console.error('\thex2json [--pretty] ') - console.error('\tjson2hex \'\'') + 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('\tbin2json [--pretty] [binary input]') + console.error('\tjson2hex \'[json input]\'') + console.error('\tjson2diag \'[json input]\'') + console.error('\tjson2bin \'[json input]\'') + console.error('Input may either be supplied as an argument or piped via stdin') process.exit(code || 0) } -if (cmd === 'help') { - usage(0) -} else if (cmd === 'hex2json') { - const argv = process.argv.filter((s) => s !== '--pretty') - const pretty = argv.length !== process.argv.length - if (argv.length < 4) { - console.error('hex2json requires a hexadecimal input string') - usage(1) - } - const bin = fromHex(argv[3]) - console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined)) -} else if (cmd === 'hex2diag') { - if (process.argv.length < 4) { - console.error('hex2diag requires a hexadecimal input string') - usage(1) - } - const bin = fromHex(process.argv[3]) - for (const line of tokensToDiagnostic(bin)) { - console.log(line) +async function fromStdin () { + const chunks = [] + for await (const chunk of process.stdin) { + chunks.push(chunk) } -} else if (cmd === 'json2hex') { - if (process.argv.length < 4) { - console.error('json2hex requires a JSON input string') - usage(1) + return Buffer.concat(chunks) +} + +/** + * @param {string} str + * @returns {Uint8Array} + */ +function fromHex (str) { + str = str.replace(/\r?\n/g, '') // let's be charitable + /* c8 ignore next 3 */ + if (!(/^([0-9a-f]{2})*$/i).test(str)) { + throw new Error('Input string is not hexadecimal format') } - const obj = JSON.parse(process.argv[3]) - console.log(toHex(encode(obj))) -} else { // no, or unknown cmd - // this is a dirty hack to allow import of this package by the tests - // for inclusion in ipjs bundling, but to silently ignore it so we don't - // print usage and exit(1). - if (process.argv.findIndex((a) => a.endsWith('mocha')) === -1) { - if (cmd) { - console.error(`Unknown command: '${cmd}'`) - } - usage(1) + return _fromHex(str) +} + +function argvPretty () { + const argv = process.argv.filter((s) => s !== '--pretty') + const pretty = argv.length !== process.argv.length + return { argv, pretty } +} + +async function run () { + const cmd = process.argv[2] + + switch (cmd) { + case 'help': { + 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]) + 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 */ + const bin = process.argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(process.argv[3]) + return console.log(toHex(bin)) + } + + case 'bin2json': { + const { argv, pretty } = argvPretty() + /* c8 ignore next 1 */ + const bin = argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(argv[3]) + return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined)) + } + + 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 'json2hex': { + const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3] + const obj = JSON.parse(inp) + return console.log(toHex(encode(obj))) + } + + case 'json2diag': { + const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3] + const obj = JSON.parse(inp) + for (const line of tokensToDiagnostic(encode(obj))) { + console.log(line) + } + return + } + + case 'json2bin': { + const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3] + const obj = JSON.parse(inp) + return process.stdout.write(encode(obj)) + } + + default: { // no, or unknown cmd + // this is a dirty hack to allow import of this package by the tests + // for inclusion in ipjs bundling, but to silently ignore it so we don't + // print usage and exit(1). + if (process.argv.findIndex((a) => a.endsWith('mocha')) === -1) { + if (cmd) { + console.error(`Unknown command: '${cmd}'`) + } + usage(1) + } + } } } +run().catch((err) => { + /* c8 ignore next 2 */ + console.error(err) + process.exit(1) +}) + // for ipjs, to get it to compile export default true diff --git a/test/node-test-bin.js b/test/node-test-bin.js index 536c24a..4def293 100644 --- a/test/node-test-bin.js +++ b/test/node-test-bin.js @@ -12,9 +12,16 @@ const { assert } = chai const binPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../lib/bin.js') -async function execBin (cmd) { +function fromHex (hex) { + return new Uint8Array(hex.split('') + .map((/** @type {string} */ c, /** @type {number} */ i, /** @type {string[]} */ d) => i % 2 === 0 ? `0x${c}${d[i + 1]}` : '') + .filter(Boolean) + .map((/** @type {string} */ e) => parseInt(e, 16))) +} + +async function execBin (cmd, stdin) { return new Promise((resolve, reject) => { - exec(`"${process.execPath}" "${binPath}" ${cmd}`, (err, stdout, stderr) => { + const cp = exec(`"${process.execPath}" "${binPath}" ${cmd}`, (err, stdout, stderr) => { if (err) { err.stdout = stdout err.stderr = stderr @@ -22,6 +29,12 @@ async function execBin (cmd) { } resolve({ stdout, stderr }) }) + if (stdin != null) { + cp.on('spawn', () => { + cp.stdin.write(stdin) + cp.stdin.end() + }) + } }) } @@ -35,9 +48,16 @@ describe('Bin', () => { assert.strictEqual(e.stderr, `Usage: cborg Valid commands: -\thex2diag -\thex2json [--pretty] -\tjson2hex '' +\thex2diag [hex input] +\thex2bin [hex input] +\thex2json [--pretty] [hex input] +\tbin2hex [binary input] +\tbin2diag [binary input] +\tbin2json [--pretty] [binary input] +\tjson2hex '[json input]' +\tjson2diag '[json input]' +\tjson2bin '[json input]' +Input may either be supplied as an argument or piped via stdin `) } }) @@ -52,9 +72,16 @@ Valid commands: `Unknown command: 'blip' Usage: cborg Valid commands: -\thex2diag -\thex2json [--pretty] -\tjson2hex '' +\thex2diag [hex input] +\thex2bin [hex input] +\thex2json [--pretty] [hex input] +\tbin2hex [binary input] +\tbin2diag [binary input] +\tbin2json [--pretty] [binary input] +\tjson2hex '[json input]' +\tjson2diag '[json input]' +\tjson2bin '[json input]' +Input may either be supplied as an argument or piped via stdin `) } }) @@ -65,30 +92,34 @@ Valid commands: assert.strictEqual(stderr, `Usage: cborg Valid commands: -\thex2diag -\thex2json [--pretty] -\tjson2hex '' +\thex2diag [hex input] +\thex2bin [hex input] +\thex2json [--pretty] [hex input] +\tbin2hex [binary input] +\tbin2diag [binary input] +\tbin2json [--pretty] [binary input] +\tjson2hex '[json input]' +\tjson2diag '[json input]' +\tjson2bin '[json input]' +Input may either be supplied as an argument or piped via stdin `) }) - it('hex2json', async () => { - const { stdout, stderr } = await execBin('hex2json a3616101616282020365736d696c6564f09f9880') - assert.strictEqual(stderr, '') - assert.strictEqual(stdout, '{"a":1,"b":[2,3],"smile":"😀"}\n') - - try { - await execBin('hex2json') - assert.fail('should have errored') - } catch (e) { - assert.strictEqual(e.stdout, '') - assert.isTrue(e.stderr.startsWith('hex2json requires a hexadecimal input string\nUsage: ')) - } - }) + for (const stdin of [true, false]) { + it(`hex2json${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('hex2json a3616101616282020365736d696c6564f09f9880') + : await execBin('hex2json', 'a3616101616282020365736d696c6564f09f9880') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, '{"a":1,"b":[2,3],"smile":"😀"}\n') + }) - it('hex2json pretty', async () => { - const { stdout, stderr } = await execBin('hex2json --pretty a3616101616282020365736d696c6564f09f9880') - assert.strictEqual(stderr, '') - assert.strictEqual(stdout, + it(`hex2json pretty${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('hex2json --pretty a3616101616282020365736d696c6564f09f9880') + : await execBin('hex2json --pretty', 'a3616101616282020365736d696c6564f09f9880') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, `{ "a": 1, "b": [ @@ -98,12 +129,14 @@ Valid commands: "smile": "😀" } `) - }) + }) - it('hex2diag', async () => { - const { stdout, stderr } = await execBin('hex2diag a4616101616282020363627566440102036165736d696c6564f09f9880') - assert.strictEqual(stderr, '') - assert.strictEqual(stdout, + it(`hex2diag${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('hex2diag a4616101616282020363627566440102036165736d696c6564f09f9880') + : await execBin('hex2diag', 'a4616101616282020363627566440102036165736d696c6564f09f9880') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, `a4 # map(4) 61 # string(1) 61 # "a" @@ -122,27 +155,99 @@ Valid commands: 64 f09f # string(2) f09f9880 # "😀" `) + }) - try { - await execBin('hex2diag') - assert.fail('should have errored') - } catch (e) { - assert.strictEqual(e.stdout, '') - assert.isTrue(e.stderr.startsWith('hex2diag requires a hexadecimal input string\nUsage: ')) - } + it(`hex2bin${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('hex2bin a3616101616282020365736d696c6564f09f9880') + : await execBin('hex2bin', 'a3616101616282020365736d696c6564f09f9880') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, new TextDecoder().decode(fromHex('a3616101616282020365736d696c6564f09f9880'))) + }) + + it(`json2hex${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('json2hex "{\\"a\\":1,\\"b\\":[2,3],\\"smile\\":\\"😀\\"}"') + : await execBin('json2hex', '{"a":1,"b":[2,3],"smile":"😀"}') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, 'a3616101616282020365736d696c6564f09f9880\n') + }) + + it(`json2bin${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('json2bin "{\\"a\\":1,\\"b\\":[2,3],\\"smile\\":\\"😀\\"}"') + : await execBin('json2bin', '{"a":1,"b":[2,3],"smile":"😀"}') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, new TextDecoder().decode(fromHex('a3616101616282020365736d696c6564f09f9880'))) + }) + + it(`json2diag${stdin ? ' (stdin)' : ''}`, async () => { + const { stdout, stderr } = stdin + ? await execBin('json2diag "{\\"a\\":1,\\"b\\":[2,3],\\"smile\\":\\"😀\\"}"') + : await execBin('json2diag', '{"a":1,"b":[2,3],"smile":"😀"}') + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, +`a3 # map(3) + 61 # string(1) + 61 # "a" + 01 # uint(1) + 61 # string(1) + 62 # "b" + 82 # array(2) + 02 # uint(2) + 03 # uint(3) + 65 # string(5) + 736d696c65 # "smile" + 64 f09f # string(2) + f09f9880 # "😀" +`) + }) + } + + it('bin2diag (stdin)', async () => { + const { stdout, stderr } = await execBin('bin2diag', fromHex('a3616101616282020365736d696c6564f09f9880')) + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, +`a3 # map(3) + 61 # string(1) + 61 # "a" + 01 # uint(1) + 61 # string(1) + 62 # "b" + 82 # array(2) + 02 # uint(2) + 03 # uint(3) + 65 # string(5) + 736d696c65 # "smile" + 64 f09f # string(2) + f09f9880 # "😀" +`) }) - it('json2hex', async () => { - const { stdout, stderr } = await execBin('json2hex "{\\"a\\":1,\\"b\\":[2,3],\\"smile\\":\\"😀\\"}"') + it('bin2json (stdin)', async () => { + const { stdout, stderr } = await execBin('bin2json', fromHex('a3616101616282020365736d696c6564f09f9880')) assert.strictEqual(stderr, '') - assert.strictEqual(stdout, 'a3616101616282020365736d696c6564f09f9880\n') + assert.strictEqual(stdout, '{"a":1,"b":[2,3],"smile":"😀"}\n') + }) - try { - await execBin('json2hex') - assert.fail('should have errored') - } catch (e) { - assert.strictEqual(e.stdout, '') - assert.isTrue(e.stderr.startsWith('json2hex requires a JSON input string\nUsage: ')) - } + it('bin2json pretty (stdin)', async () => { + const { stdout, stderr } = await execBin('bin2json --pretty', fromHex('a3616101616282020365736d696c6564f09f9880')) + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, +`{ + "a": 1, + "b": [ + 2, + 3 + ], + "smile": "😀" +} +`) + }) + + it('bin2hex (stdin)', async () => { + const { stdout, stderr } = await execBin('bin2hex', fromHex('a3616101616282020365736d696c6564f09f9880')) + assert.strictEqual(stderr, '') + assert.strictEqual(stdout, 'a3616101616282020365736d696c6564f09f9880\n') }) })