diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 51104e9..966291f 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -88,6 +88,18 @@ describe('copyfiles', () => { }); })); + test('ignores inherited option properties', () => + new Promise((done: any) => { + writeFileSync('input/a.txt', 'a'); + const options = Object.create({ flat: true }); + + copyfiles('input/a.txt', 'output', options, () => { + expect(existsSync('output/input/a.txt')).toBe(true); + expect(existsSync('output/a.txt')).toBe(false); + done(); + }); + })); + test('copies files using directory pattern with /**', () => new Promise((done: any) => { writeFileSync('input/a.txt', 'a'); @@ -789,6 +801,12 @@ describe('copyfiles', () => { expect(result.replaceAll('\\', '/').endsWith(posixJoin('dest', 'bar.txt'))).toBe(true); }); + it('getDestinationPath ignores inherited option properties', () => { + const options = Object.create({ flat: true }); + const result = getDestinationPath('foo/bar.txt', 'dest', options, false); + expect(result.replaceAll('\\', '/')).toBe(posixJoin('dest', 'foo', 'bar.txt')); + }); + it('getDestinationPath - up === true branch', () => { const result = getDestinationPath('foo/bar.txt', 'dest', { up: true }, false); expect(result.replaceAll('\\', '/').endsWith(posixJoin('dest', 'bar.txt'))).toBe(true); diff --git a/src/cli.ts b/src/cli.ts index 5334508..c926163 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,6 +24,57 @@ function handleError(err?: Error) { } try { + // cli-nano resolves option names through dynamic property access. Keep this + // lookup table free of inherited keys such as "__proto__" and "constructor". + const optionDefinitions = { + all: { + alias: 'a', + type: 'boolean', + describe: 'Include files & directories begining with a dot (.)', + }, + dryRun: { + alias: 'd', + type: 'boolean', + describe: 'Show what would be copied, but do not actually copy any files', + }, + error: { + alias: 'E', + type: 'boolean', + describe: 'Throw error if nothing is copied', + }, + exclude: { + alias: 'e', + type: 'array', + describe: 'Pattern or glob to exclude (may be passed multiple times)', + }, + flat: { + alias: 'f', + type: 'boolean', + describe: 'Flatten the output', + }, + follow: { + alias: 'F', + type: 'boolean', + describe: 'Follow symbolink links', + }, + stat: { + alias: 's', + type: 'boolean', + describe: 'Show statistics after execution (execution time + file count)', + }, + up: { + alias: 'u', + type: 'number', + describe: 'Slice a path off the bottom of the paths', + }, + verbose: { + alias: 'V', + type: 'boolean', + describe: 'Print more information to console', + }, + } as const; + const options = Object.assign(Object.create(null) as typeof optionDefinitions, optionDefinitions); + const config = { command: { name: 'copyfiles', @@ -51,53 +102,7 @@ try { }, ], }, - options: { - all: { - alias: 'a', - type: 'boolean', - describe: 'Include files & directories begining with a dot (.)', - }, - dryRun: { - alias: 'd', - type: 'boolean', - describe: 'Show what would be copied, but do not actually copy any files', - }, - error: { - alias: 'E', - type: 'boolean', - describe: 'Throw error if nothing is copied', - }, - exclude: { - alias: 'e', - type: 'array', - describe: 'Pattern or glob to exclude (may be passed multiple times)', - }, - flat: { - alias: 'f', - type: 'boolean', - describe: 'Flatten the output', - }, - follow: { - alias: 'F', - type: 'boolean', - describe: 'Follow symbolink links', - }, - stat: { - alias: 's', - type: 'boolean', - describe: 'Show statistics after execution (execution time + file count)', - }, - up: { - alias: 'u', - type: 'number', - describe: 'Slice a path off the bottom of the paths', - }, - verbose: { - alias: 'V', - type: 'boolean', - describe: 'Print more information to console', - }, - }, + options, version: readPackage().version, } as const; diff --git a/src/index.ts b/src/index.ts index 73d8445..fd6248c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,10 @@ function throwOrCallback(err: Error, cb?: (e?: Error) => void) { } } +function createSafeOptions(options: CopyFileOptions): CopyFileOptions { + return Object.assign(Object.create(null) as CopyFileOptions, options); +} + function callRenameWhenDefined(inFile: string, dest: string, options: CopyFileOptions): string { if (typeof options.rename === 'function') { return options.rename(inFile, dest); @@ -51,6 +55,7 @@ function callRenameWhenDefined(inFile: string, dest: string, options: CopyFileOp * Calculate the destination path for a given input file and options. */ export function getDestinationPath(inFile: string, outDir: string, options: CopyFileOptions, isSingleFileRename = false): string { + options = createSafeOptions(options); const fileDir = dirname(inFile); const fileName = basename(inFile); const srcExt = extname(fileName); @@ -184,6 +189,9 @@ function getMatchedFiles( * @param {(e?: Error) => void} callback - optionally callback that will be executed after copy is finished or when an error occurs */ export function copyfiles(sources: string | string[], outPath: string, options: CopyFileOptions = {}, callback?: (e?: Error) => void) { + // Treat options as data rather than inheriting behavior from Object.prototype. + // This also safely preserves an own "__proto__" key if options came from JSON. + options = createSafeOptions(options); const cb = callback || options.callback; sources = arrify(sources);