diff --git a/node/cli.js b/node/cli.js index bfba762a..cd8ceacc 100644 --- a/node/cli.js +++ b/node/cli.js @@ -1,7 +1,8 @@ #!/usr/bin/env node 'use strict' -const { readFile, writeFile } = require('node:fs/promises') +const { readFile, stat, writeFile } = require('node:fs/promises') +const { resolve } = require('node:path') const FORMATS = 'doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv' @@ -113,11 +114,33 @@ async function readStdin() { return Buffer.concat(chunks) } +async function referToSameFile(input, output) { + if (resolve(input) === resolve(output)) return true + try { + const [input_stats, output_stats] = await Promise.all([stat(input), stat(output)]) + return input_stats.dev === output_stats.dev && input_stats.ino === output_stats.ino + } catch (error) { + if (error.code === 'ENOENT') return false + throw error + } +} + async function main() { const args = parseArgs(process.argv.slice(2)) if (args.input === null) { fail(USAGE_ERROR, 'missing input: pass a document path, or - for stdin (see anydoc --help)') } + if (args.input !== '-' && args.output !== null) { + let same_file + try { + same_file = await referToSameFile(args.input, args.output) + } catch (error) { + fail(CONVERSION_ERROR, error.message) + } + if (same_file) { + fail(USAGE_ERROR, 'output path must differ from the input path') + } + } // Loaded after argument handling so --help and --version work even where // no native binding is available. diff --git a/node/test.mjs b/node/test.mjs index ec98a3c6..932c784d 100644 --- a/node/test.mjs +++ b/node/test.mjs @@ -1,7 +1,7 @@ // Smoke test: the bindings load and every entry point round-trips a fixture. import assert from 'node:assert/strict' import { execFile } from 'node:child_process' -import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { copyFile, link, mkdtemp, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -111,6 +111,42 @@ test('cli writes to --output instead of stdout', async () => { } }) +test('cli refuses to overwrite its input file', async () => { + const dir = await mkdtemp(join(tmpdir(), 'anydoc-cli-')) + try { + const input = join(dir, 'input.docx') + await copyFile(OUTLINE, input) + const before = await readFile(input) + + const { code, stderr } = await runCli([input, '--output', input]) + + assert.equal(code, 2) + assert.match(stderr, /output.*input/i) + assert.deepEqual(await readFile(input), before) + } finally { + await rm(dir, { recursive: true, force: true }) + } +}) + +test('cli refuses to overwrite its input through a hard link', async () => { + const dir = await mkdtemp(join(tmpdir(), 'anydoc-cli-')) + try { + const input = join(dir, 'input.docx') + const output = join(dir, 'output.md') + await copyFile(OUTLINE, input) + await link(input, output) + const before = await readFile(input) + + const { code, stderr } = await runCli([input, '--output', output]) + + assert.equal(code, 2) + assert.match(stderr, /output.*input/i) + assert.deepEqual(await readFile(input), before) + } finally { + await rm(dir, { recursive: true, force: true }) + } +}) + test('cli reads stdin with an explicit format', async () => { const child = promisify(execFile)(process.execPath, [CLI, '-', '--format', 'csv']) child.child.stdin.end(await readFile(CSV))