@adguard/diff-builder
Advanced tools
Comparing version
@@ -8,3 +8,11 @@ # Diff Builder Changelog | ||
## [1.1.0] - 2025-03-13 | ||
### Changed | ||
- Switched from js 'diff' to system GNU utility 'diff' [#4]. | ||
[#4]: https://github.com/AdguardTeam/DiffBuilder/issues/4 | ||
[1.1.0]: https://github.com/AdguardTeam/DiffBuilder/compare/v1.0.18...v1.1.0 | ||
## [1.0.18] - 2024-04-04 | ||
@@ -11,0 +19,0 @@ |
@@ -1,3 +0,1 @@ | ||
/// <reference path="../../../types/diff.d.ts" /> | ||
import { TypesOfChanges } from '../common/types-of-change'; | ||
import { Resolution } from '../common/patch-name'; | ||
@@ -56,18 +54,12 @@ export declare const PATCH_EXTENSION = ".patch"; | ||
/** | ||
* Detects type of diff changes: add or delete. | ||
* Creates patch in [RCS format](https://www.gnu.org/software/diffutils/manual/diffutils.html#RCS). | ||
* | ||
* @param line Line of string to parse. | ||
* @param oldContent Content of old file. | ||
* @param newContent Content of new file. | ||
* | ||
* @returns String type: 'Add' or 'Delete' or null if type cannot be parsed. | ||
*/ | ||
export declare const detectTypeOfChanges: (line: string) => TypesOfChanges | null; | ||
/** | ||
* Creates patch in [RCS format](https://www.gnu.org/software/diffutils/manual/diffutils.html#RCS). | ||
* @throws Error if the patch creation fails. | ||
* | ||
* @param oldFile Old file. | ||
* @param newFile New file. | ||
* | ||
* @returns Difference between old and new files in RCS format. | ||
* @returns Promise resolving to the difference in RCS format. | ||
*/ | ||
export declare const createPatch: (oldFile: string, newFile: string) => string; | ||
export declare const createPatch: (oldContent: string, newContent: string) => Promise<string>; | ||
/** | ||
@@ -74,0 +66,0 @@ * Checks if the provided file content contains a checksum tag within its first 200 characters. |
{ | ||
"name": "@adguard/diff-builder", | ||
"version": "1.0.18", | ||
"version": "1.1.0", | ||
"description": "A tool for generating differential updates for filter lists.", | ||
@@ -50,3 +50,2 @@ "repository": { | ||
"lint": "eslint --cache . && tsc --noEmit", | ||
"diff-builder": "node -r esbuild-register cli/index.ts build -c", | ||
"test": "jest" | ||
@@ -56,4 +55,3 @@ }, | ||
"commander": "^11.1.0", | ||
"crypto-js": "^4.2.0", | ||
"diff": "git+https://github.com/105th/jsdiff.git#2be2e7df90e8eebd99f0385c7b1dc16c2f4dcc1a" | ||
"crypto-js": "^4.2.0" | ||
}, | ||
@@ -68,3 +66,2 @@ "devDependencies": { | ||
"@types/crypto-js": "^4.2.1", | ||
"@types/diff": "^5.0.7", | ||
"@types/express": "^4.17.21", | ||
@@ -71,0 +68,0 @@ "@types/jest": "^29.5.6", |
@@ -5,12 +5,24 @@ # AdGuard Diff Builder | ||
- [How to install](#how-to-install) | ||
- [How to use](#how-to-use) | ||
- [Use as CLI](#cli) | ||
- [Use as API](#api) | ||
## Contents | ||
- [Prerequisites](#prerequisites) | ||
- [How to Install](#how-to-install) | ||
- [How to Use](#how-to-use) | ||
- [CLI](#cli) | ||
- [API](#api) | ||
- [Algorithm](#algorithm) | ||
## How to install | ||
## Prerequisites | ||
`yarn add @adguard/diff-builder` | ||
- **diff utility**: This tool relies on the standard Unix `diff` utility to generate patches efficiently. Make sure it's installed on your system. | ||
- On macOS: Available by default or through XCode CLI tools | ||
- On Linux: Available by default or install via your package manager (e.g., `apt-get install diffutils`) | ||
- On Windows: Available via WSL or Git Bash | ||
## How to Install | ||
```bash | ||
yarn add @adguard/diff-builder | ||
``` | ||
## How to Use | ||
@@ -17,0 +29,0 @@ |
@@ -1,11 +0,5 @@ | ||
/// <reference path="../../types/diff.d.ts" /> | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
// eslint-disable-next-line import/extensions | ||
import { structuredPatch } from 'diff/src/index.js'; | ||
import { CHECKSUM_TAG, DIFF_PATH_TAG } from '../common/constants'; | ||
import { TypesOfChanges } from '../common/types-of-change'; | ||
import { createDiffDirective, parseDiffDirective } from '../common/diff-directive'; | ||
@@ -27,7 +21,9 @@ import { calculateChecksumMD5 } from '../common/calculate-checksum'; | ||
import { applyRcsPatch } from '../diff-updater/update'; | ||
import { getErrorMessage } from '../common/get-error-message'; | ||
import { spawnDiff } from '../common/spawn-diff'; | ||
import { getTempFilePaths } from '../common/get-temp-file-paths'; | ||
import { writeToTempFiles, deleteTempFiles } from '../common/temp-files-utils'; | ||
const DEFAULT_PATCH_TTL_SECONDS = 60 * 60 * 24 * 7; | ||
const NEW_LINE_INFO = '\\ No newline at end of file'; | ||
let log: (message: string) => void; | ||
@@ -97,138 +93,37 @@ | ||
/** | ||
* Detects type of diff changes: add or delete. | ||
* Creates patch in [RCS format](https://www.gnu.org/software/diffutils/manual/diffutils.html#RCS). | ||
* | ||
* @param line Line of string to parse. | ||
* @param oldContent Content of old file. | ||
* @param newContent Content of new file. | ||
* | ||
* @returns String type: 'Add' or 'Delete' or null if type cannot be parsed. | ||
*/ | ||
export const detectTypeOfChanges = (line: string): TypesOfChanges | null => { | ||
if (line.startsWith('+')) { | ||
return TypesOfChanges.Add; | ||
} | ||
if (line.startsWith('-')) { | ||
return TypesOfChanges.Delete; | ||
} | ||
return null; | ||
}; | ||
/** | ||
* Creates patch in [RCS format](https://www.gnu.org/software/diffutils/manual/diffutils.html#RCS). | ||
* @throws Error if the patch creation fails. | ||
* | ||
* @param oldFile Old file. | ||
* @param newFile New file. | ||
* | ||
* @returns Difference between old and new files in RCS format. | ||
* @returns Promise resolving to the difference in RCS format. | ||
*/ | ||
export const createPatch = (oldFile: string, newFile: string): string => { | ||
const { hunks } = structuredPatch( | ||
'oldFile', | ||
'newFile', | ||
oldFile, | ||
newFile, | ||
'', | ||
'', | ||
{ | ||
context: 0, | ||
ignoreCase: false, | ||
}, | ||
); | ||
export const createPatch = async (oldContent: string, newContent: string): Promise<string> => { | ||
const tempFiles = getTempFilePaths(); | ||
const outDiff: string[] = []; | ||
try { | ||
// Wait for files to be written before creating diff | ||
await writeToTempFiles({ | ||
oldFilePath: tempFiles.oldFilePath, | ||
oldContent, | ||
newFilePath: tempFiles.newFilePath, | ||
newContent, | ||
}); | ||
let stringsToAdd: string[] = []; | ||
let nStringsToDelete = 0; | ||
// Generate the diff using our utility function | ||
const patch = await spawnDiff(tempFiles.oldFilePath, tempFiles.newFilePath); | ||
const collectParsedDiffBlock = ( | ||
curIndex: number, | ||
deleted: number, | ||
added: string[], | ||
): string[] => { | ||
if (deleted > 0) { | ||
const deleteFromPosition = curIndex; | ||
const rcsLines = [`d${deleteFromPosition} ${deleted}`]; | ||
return rcsLines; | ||
} | ||
if (added.length > 0) { | ||
const addFromPosition = curIndex - 1; | ||
const rcsLines = [ | ||
`a${addFromPosition} ${added.length}`, | ||
...added, | ||
]; | ||
return rcsLines; | ||
} | ||
return []; | ||
}; | ||
hunks.forEach((hunk, hunkIdx) => { | ||
const { oldStart, lines } = hunk; | ||
let fileIndexScanned = oldStart; | ||
// Library will print some debug info so we need to skip this line. | ||
const filteredLines = lines.filter((l) => l !== NEW_LINE_INFO); | ||
for (let index = 0; index < filteredLines.length; index += 1) { | ||
const line = filteredLines[index]; | ||
// Detect type of diff operation | ||
const typeOfChange = detectTypeOfChanges(line); | ||
// Library will print some debug info so we need to skip this line. | ||
if (typeOfChange === null) { | ||
throw new Error(`Cannot parse line: ${line}`); | ||
} | ||
if (typeOfChange === TypesOfChanges.Delete) { | ||
// In RCS format we don't need content of deleted string. | ||
nStringsToDelete += 1; | ||
} | ||
if (typeOfChange === TypesOfChanges.Add) { | ||
// Slice "+" from the start of string. | ||
stringsToAdd.push(line.slice(1)); | ||
} | ||
// Check type of next line for possible change diff type from 'add' | ||
// to 'delete' or from 'delete' to 'add'. | ||
const nextLineTypeOfChange = index + 1 < filteredLines.length | ||
? detectTypeOfChanges(filteredLines[index + 1]) | ||
: null; | ||
// If type will change - save current block | ||
const typeWillChangeOnNextLine = nextLineTypeOfChange && typeOfChange !== nextLineTypeOfChange; | ||
// Or if current line is the last - we need to save collected info. | ||
const isLastLine = index === filteredLines.length - 1; | ||
if (typeWillChangeOnNextLine || isLastLine) { | ||
const diffRCSLines = collectParsedDiffBlock( | ||
fileIndexScanned, | ||
nStringsToDelete, | ||
stringsToAdd, | ||
); | ||
outDiff.push(...diffRCSLines); | ||
// Drop counters | ||
nStringsToDelete = 0; | ||
stringsToAdd = []; | ||
// Move scanned index | ||
fileIndexScanned += index + 1; | ||
} | ||
} | ||
// Check if we need to insert new line to the patch or not | ||
if ((lines.filter((l) => l === NEW_LINE_INFO).length > 0 && lines[lines.length - 1] !== NEW_LINE_INFO) | ||
|| (lines.filter((l) => l === NEW_LINE_INFO).length === 0 && hunkIdx === hunks.length - 1)) { | ||
outDiff[outDiff.length - 1] = outDiff[outDiff.length - 1].concat('\n'); | ||
} | ||
}); | ||
return outDiff.join('\n'); | ||
return patch; | ||
} catch (e) { | ||
log(`Failed to create a patch: ${getErrorMessage(e)}`); | ||
throw e; | ||
} finally { | ||
await deleteTempFiles(tempFiles.oldFilePath, tempFiles.newFilePath, log); | ||
} | ||
}; | ||
/** | ||
* Scans `absolutePatchesPath` for files with the "*.patch" pattern and deletes | ||
* Scans `absolutePatchesPath` for files with the `*.${PATCH_EXTENSION}` pattern and deletes | ||
* those whose created epoch timestamp has expired and whose are not empty. | ||
@@ -239,2 +134,4 @@ * | ||
* | ||
* @see {@link PATCH_EXTENSION} | ||
* | ||
* @returns Returns number of deleted patches. | ||
@@ -373,9 +270,15 @@ */ | ||
const updatedFile = applyRcsPatch( | ||
splitByLines(oldFile), | ||
diffDirective ? patchLines.slice(1) : patchLines, | ||
diffDirective ? diffDirective.checksum : undefined, | ||
); | ||
try { | ||
const updatedFile = applyRcsPatch( | ||
splitByLines(oldFile), | ||
diffDirective ? patchLines.slice(1) : patchLines, | ||
diffDirective ? diffDirective.checksum : undefined, | ||
); | ||
return updatedFile === newFile; | ||
return updatedFile === newFile; | ||
} catch (e) { | ||
log(`Failed to apply patch to the old file: ${getErrorMessage(e)}`); | ||
return false; | ||
} | ||
}; | ||
@@ -464,3 +367,3 @@ | ||
// Generate the diff patch. | ||
let patch = createPatch(oldFile, newFileWithUpdatedTags); | ||
let patch = await createPatch(oldFile, newFileWithUpdatedTags); | ||
@@ -467,0 +370,0 @@ // Optionally add a checksum to the patch. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Git dependency
Supply chain riskContains a dependency which resolves to a remote git URL. Dependencies fetched from git URLs are not immutable and can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
2
-33.33%27
-3.57%43
16.22%142
9.23%0
-100%356339
-6.76%7196
-4.88%3
200%