diff --git a/package.json b/package.json index afd8e9a..29e3691 100644 --- a/package.json +++ b/package.json @@ -17,17 +17,17 @@ "build/src" ], "scripts": { - "prepublishOnly": "npm run clean && npm run build", + "prepublishOnly": "npm run clean && npm run build && npm run lint", "clean": "rm -rf build && rm -rf .nyc_output && rm -rf coverage", "copydeps": "copyfiles \"test/expected/**\" test/testdir.tar \"test/extended/res/**\" build", - "build": "tsc && npm run lint && npm run copydeps", + "build": "tsc && npm run copydeps", "lint": "eslint ./src ./test --ext .ts,.js", "pretest": "npm install && npm run build", "test": "node build/test/runTests.js", "extest": "npm run pretest && ./test/extended/init.sh && test/extended/runall.sh", "coverage": "npx nyc --exclude \"build/test/**\" --reporter=lcov npm test && npx nyc report", "toc": "npx markdown-toc README.md; echo \n", - "docs": "typedoc --includeVersion --excludeExternals --theme minimal --readme none --gitRevision master --toc compare,compareSync,fileCompareHandlers,Options,Result --out docs ./src/index.d.ts" + "docs": "typedoc --includeVersion --excludeExternals --theme minimal --readme none --gitRevision master --toc compare,compareSync,fileCompareHandlers,Options,Result --out docs ./src/index.ts" }, "dependencies": { "buffer-equal": "^1.0.0", diff --git a/src/FileCompareHandlers.ts b/src/FileCompareHandlers.ts new file mode 100644 index 0000000..1ff4e60 --- /dev/null +++ b/src/FileCompareHandlers.ts @@ -0,0 +1,21 @@ +import { CompareFileHandler } from './types'; + + +export interface FileCompareHandlers { + /** + * Default file content comparison handlers, used if [[Options.compareFileAsync]] or [[Options.compareFileSync]] are not specified. + * + * Performs binary comparison. + */ + defaultFileCompare: CompareFileHandler; + /** + * Compares files line by line. + * + * Options: + * * ignoreLineEnding - true/false (default: false) - Ignore cr/lf line endings + * * ignoreWhiteSpaces - true/false (default: false) - Ignore white spaces at the beginning and ending of a line (similar to 'diff -b') + * * ignoreAllWhiteSpaces - true/false (default: false) - Ignore all white space differences (similar to 'diff -w') + * * ignoreEmptyLines - true/false (default: false) - Ignores differences caused by empty lines (similar to 'diff -B') + */ + lineBasedFileCompare: CompareFileHandler; +} diff --git a/src/fileCompareHandler/default/defaultFileCompare.ts b/src/fileCompareHandler/default/defaultFileCompare.ts index b8f44c2..74ed838 100644 --- a/src/fileCompareHandler/default/defaultFileCompare.ts +++ b/src/fileCompareHandler/default/defaultFileCompare.ts @@ -5,91 +5,95 @@ import fsPromise from '../../fs/fsPromise' import { BufferPair, BufferPool } from '../../fs/BufferPool' import closeFile from '../../fs/closeFile' import { Options } from '../../index' +import { CompareFileHandler } from '../../types' const MAX_CONCURRENT_FILE_COMPARE = 8 const BUF_SIZE = 100000 const fdQueue = new FileDescriptorQueue(MAX_CONCURRENT_FILE_COMPARE * 2) const bufferPool = new BufferPool(BUF_SIZE, MAX_CONCURRENT_FILE_COMPARE) // fdQueue guarantees there will be no more than MAX_CONCURRENT_FILE_COMPARE async processes accessing the buffers concurrently - -/** - * Compares two files by content. - */ -function compareSync(path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): boolean { - let fd1: number | undefined - let fd2: number | undefined - if (stat1.size !== stat2.size) { - return false - } - const bufferPair = bufferPool.allocateBuffers() - try { - fd1 = fs.openSync(path1, 'r') - fd2 = fs.openSync(path2, 'r') - const buf1 = bufferPair.buf1 - const buf2 = bufferPair.buf2 - for (; ;) { - const size1 = fs.readSync(fd1, buf1, 0, BUF_SIZE, null) - const size2 = fs.readSync(fd2, buf2, 0, BUF_SIZE, null) - if (size1 !== size2) { - return false - } else if (size1 === 0) { - // End of file reached - return true - } else if (!compareBuffers(buf1, buf2, size1)) { - return false +export const defaultFileCompare: CompareFileHandler = { + /** + * Compares two files by content. + */ + compareSync(path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): boolean { + let fd1: number | undefined + let fd2: number | undefined + if (stat1.size !== stat2.size) { + return false + } + const bufferPair = bufferPool.allocateBuffers() + try { + fd1 = fs.openSync(path1, 'r') + fd2 = fs.openSync(path2, 'r') + const buf1 = bufferPair.buf1 + const buf2 = bufferPair.buf2 + for (; ;) { + const size1 = fs.readSync(fd1, buf1, 0, BUF_SIZE, null) + const size2 = fs.readSync(fd2, buf2, 0, BUF_SIZE, null) + if (size1 !== size2) { + return false + } else if (size1 === 0) { + // End of file reached + return true + } else if (!compareBuffers(buf1, buf2, size1)) { + return false + } } + } finally { + closeFile.closeFilesSync(fd1, fd2) + bufferPool.freeBuffers(bufferPair) } - } finally { - closeFile.closeFilesSync(fd1, fd2) - bufferPool.freeBuffers(bufferPair) - } -} + }, -/** - * Compares two files by content - */ -function compareAsync(path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): Promise { - let fd1: number | undefined - let fd2: number | undefined - let bufferPair: BufferPair | undefined - if (stat1.size !== stat2.size) { - return Promise.resolve(false) + /** + * Compares two files by content + */ + async compareAsync(path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): Promise { + let fd1: number | undefined + let fd2: number | undefined + let bufferPair: BufferPair | undefined + if (stat1.size !== stat2.size) { + return Promise.resolve(false) + } + return Promise.all([fdQueue.openPromise(path1, 'r'), fdQueue.openPromise(path2, 'r')]) + .then(fds => { + bufferPair = bufferPool.allocateBuffers() + fd1 = fds[0] + fd2 = fds[1] + const buf1 = bufferPair.buf1 + const buf2 = bufferPair.buf2 + const compareAsyncInternal = () => Promise.all([ + fsPromise.read(fd1, buf1, 0, BUF_SIZE, null), + fsPromise.read(fd2, buf2, 0, BUF_SIZE, null) + ]) + .then((bufferSizes) => { + const size1 = bufferSizes[0] + const size2 = bufferSizes[1] + if (size1 !== size2) { + return false + } else if (size1 === 0) { + // End of file reached + return true + } else if (!compareBuffers(buf1, buf2, size1)) { + return false + } else { + return compareAsyncInternal() + } + }) + return compareAsyncInternal() + }) + .then( + // 'finally' polyfill for node 8 and below + res => finalizeAsync(fd1, fd2, bufferPair).then(() => res), + err => finalizeAsync(fd1, fd2, bufferPair).then(() => { throw err }) + ) } - return Promise.all([fdQueue.openPromise(path1, 'r'), fdQueue.openPromise(path2, 'r')]) - .then(fds => { - bufferPair = bufferPool.allocateBuffers() - fd1 = fds[0] - fd2 = fds[1] - const buf1 = bufferPair.buf1 - const buf2 = bufferPair.buf2 - const compareAsyncInternal = () => Promise.all([ - fsPromise.read(fd1, buf1, 0, BUF_SIZE, null), - fsPromise.read(fd2, buf2, 0, BUF_SIZE, null) - ]) - .then((bufferSizes) => { - const size1 = bufferSizes[0] - const size2 = bufferSizes[1] - if (size1 !== size2) { - return false - } else if (size1 === 0) { - // End of file reached - return true - } else if (!compareBuffers(buf1, buf2, size1)) { - return false - } else { - return compareAsyncInternal() - } - }) - return compareAsyncInternal() - }) - .then( - // 'finally' polyfill for node 8 and below - res => finalizeAsync(fd1, fd2, bufferPair).then(() => res), - err => finalizeAsync(fd1, fd2, bufferPair).then(() => { throw err }) - ) + } + function compareBuffers(buf1: Buffer, buf2: Buffer, contentSize: number) { return bufferEqual(buf1.slice(0, contentSize), buf2.slice(0, contentSize)) } @@ -101,6 +105,3 @@ function finalizeAsync(fd1?: number, fd2?: number, bufferPair?: BufferPair) { return closeFile.closeFilesAsync(fd1, fd2, fdQueue) } -export default { - compareSync, compareAsync -} diff --git a/src/fileCompareHandler/lines/compareAsync.ts b/src/fileCompareHandler/lines/compareAsync.ts index 0034317..659bc37 100644 --- a/src/fileCompareHandler/lines/compareAsync.ts +++ b/src/fileCompareHandler/lines/compareAsync.ts @@ -8,6 +8,7 @@ import { LineBasedCompareContext } from './LineBasedCompareContext' import { BufferPool } from '../../fs/BufferPool' import { compareLineBatches } from './compare/compareLineBatches' import { readBufferedLines } from './lineReader/readBufferedLines' +import { CompareFileAsync } from '../../types' const BUF_SIZE = 100000 const MAX_CONCURRENT_FILE_COMPARE = 8 @@ -15,7 +16,7 @@ const MAX_CONCURRENT_FILE_COMPARE = 8 const fdQueue = new FileDescriptorQueue(MAX_CONCURRENT_FILE_COMPARE * 2) const bufferPool = new BufferPool(BUF_SIZE, MAX_CONCURRENT_FILE_COMPARE) // fdQueue guarantees there will be no more than MAX_CONCURRENT_FILE_COMPARE async processes accessing the buffers concurrently -export default async function compareAsync(path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): Promise { +export const lineBasedCompareAsync: CompareFileAsync = async (path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): Promise => { const bufferSize = Math.min(BUF_SIZE, options.lineBasedHandlerBufferSize ?? Number.MAX_VALUE) let context: LineBasedCompareContext | undefined try { diff --git a/src/fileCompareHandler/lines/compareSync.ts b/src/fileCompareHandler/lines/compareSync.ts index ea17215..de09b92 100644 --- a/src/fileCompareHandler/lines/compareSync.ts +++ b/src/fileCompareHandler/lines/compareSync.ts @@ -6,6 +6,7 @@ import { compareLineBatches } from './compare/compareLineBatches' import { readBufferedLines } from './lineReader/readBufferedLines' import { BufferPair } from '../../fs/BufferPool' import { LineBatch } from './lineReader/LineBatch' +import { CompareFileSync } from '../../types' const BUF_SIZE = 100000 @@ -15,7 +16,7 @@ const bufferPair: BufferPair = { busy: true } -export default function compareSync(path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): boolean { +export const lineBasedCompareSync: CompareFileSync = (path1: string, stat1: fs.Stats, path2: string, stat2: fs.Stats, options: Options): boolean => { const bufferSize = Math.min(BUF_SIZE, options.lineBasedHandlerBufferSize ?? Number.MAX_VALUE) let context: LineBasedCompareContext | undefined try { diff --git a/src/fileCompareHandler/lines/lineBasedFileCompare.ts b/src/fileCompareHandler/lines/lineBasedFileCompare.ts index f72093b..d1c37b3 100644 --- a/src/fileCompareHandler/lines/lineBasedFileCompare.ts +++ b/src/fileCompareHandler/lines/lineBasedFileCompare.ts @@ -1,12 +1,12 @@ -import compareSync from './compareSync' -import compareAsync from './compareAsync' - - +import {lineBasedCompareSync} from './compareSync' +import {lineBasedCompareAsync} from './compareAsync' +import { CompareFileHandler } from '../../types' /** * Compare files line by line with options to ignore * line endings and white space differences. */ -export default { - compareSync, compareAsync +export const lineBasedFileCompare: CompareFileHandler = { + compareSync: lineBasedCompareSync, + compareAsync: lineBasedCompareAsync } diff --git a/src/index.ts b/src/index.ts index 4f8b899..f9f8f4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,46 +3,19 @@ import fs from 'fs' import compareSyncInternal from './compareSync' import compareAsyncInternal from './compareAsync' import defaultResultBuilderCallback from './resultBuilder/defaultResultBuilderCallback' -import defaultFileCompare from './fileCompareHandler/default/defaultFileCompare' -import lineBasedFileCompare from './fileCompareHandler/lines/lineBasedFileCompare' +import { defaultFileCompare } from './fileCompareHandler/default/defaultFileCompare' +import { lineBasedFileCompare } from './fileCompareHandler/lines/lineBasedFileCompare' import defaultNameCompare from './nameCompare/defaultNameCompare' import entryBuilder from './entry/entryBuilder' import statsLifecycle from './statistics/statisticsLifecycle' import loopDetector from './symlink/loopDetector' -import { CompareFileHandler, Options, Result } from './types' +import { Options, Result } from './types' +import { FileCompareHandlers } from './FileCompareHandlers' const ROOT_PATH = pathUtils.sep export * from './types' -/** - * Available file content comparison handlers. - * These handlers are used when [[Options.compareContent]] is set. - */ -export interface FileCompareHandlers { - /** - * Default file content comparison handlers, used if [[Options.compareFileAsync]] or [[Options.compareFileSync]] are not specified. - * - * Performs binary comparison. - */ - defaultFileCompare: CompareFileHandler, - /** - * Compares files line by line. - * - * Options: - * * ignoreLineEnding - true/false (default: false) - Ignore cr/lf line endings - * * ignoreWhiteSpaces - true/false (default: false) - Ignore white spaces at the beginning and ending of a line (similar to 'diff -b') - * * ignoreAllWhiteSpaces - true/false (default: false) - Ignore all white space differences (similar to 'diff -w') - * * ignoreEmptyLines - true/false (default: false) - Ignores differences caused by empty lines (similar to 'diff -B') - */ - lineBasedFileCompare: CompareFileHandler -} - -export const fileCompareHandlers: FileCompareHandlers = { - defaultFileCompare: defaultFileCompare, - lineBasedFileCompare: lineBasedFileCompare -} - /** * Synchronously compares given paths. * @param path1 Left file or directory to be compared. @@ -69,22 +42,6 @@ export function compareSync(path1: string, path2: string, options?: Options): Re return statistics as unknown as Result } -type RealPathOptions = { encoding?: BufferEncoding | null } | BufferEncoding - -const wrapper = { - realPath(path: string, options?: RealPathOptions): Promise { - return new Promise((resolve, reject) => { - fs.realpath(path, options, (err, resolvedPath) => { - if (err) { - reject(err) - } else { - resolve(resolvedPath) - } - }) - }) - } -} - /** * Asynchronously compares given paths. * @param path1 Left file or directory to be compared. @@ -125,6 +82,31 @@ export function compare(path1: string, path2: string, options?: Options): Promis }) } +/** + * File comparison handlers. + * These handlers are used when [[Options.compareContent]] is set. + */ +export const fileCompareHandlers: FileCompareHandlers = { + defaultFileCompare, + lineBasedFileCompare +} + +type RealPathOptions = { encoding?: BufferEncoding | null } | BufferEncoding + +const wrapper = { + realPath(path: string, options?: RealPathOptions): Promise { + return new Promise((resolve, reject) => { + fs.realpath(path, options, (err, resolvedPath) => { + if (err) { + reject(err) + } else { + resolve(resolvedPath) + } + }) + }) + } +} + function prepareOptions(options?: Options): Options { options = options || {} const clone = JSON.parse(JSON.stringify(options))