@adguard/diff-builder
Advanced tools
Comparing version 1.0.3 to 1.0.4
@@ -8,3 +8,12 @@ # Diff Builder Changelog | ||
## [1.0.4] - 2023-12-25 | ||
### Changed | ||
- The algorithm has been modified to ignore changes in the 'Diff-Path' and | ||
'Checksum' tags, but it now accounts for the presence of the 'Checksum' tag | ||
in the new file and recalculates it if necessary. Additionally, cases where | ||
two checksums are present in a file have been considered, and the algorithm | ||
has been simplified accordingly. | ||
## [1.0.3] - 2023-12-20 | ||
@@ -11,0 +20,0 @@ |
@@ -5,29 +5,2 @@ 'use strict'; | ||
/** | ||
* Finds value of specified header tag in filter rules text. | ||
* | ||
* @param tagName Filter header tag name. | ||
* @param rules Lines of filter rules text. | ||
* | ||
* @returns Trimmed value of specified header tag or null if tag not found. | ||
*/ | ||
const parseTag = (tagName, rules) => { | ||
// Lines of filter metadata to parse | ||
const AMOUNT_OF_LINES_TO_PARSE = 50; | ||
// Look up no more than 50 first lines | ||
const maxLines = Math.min(AMOUNT_OF_LINES_TO_PARSE, rules.length); | ||
for (let i = 0; i < maxLines; i += 1) { | ||
const rule = rules[i]; | ||
if (!rule) { | ||
continue; | ||
} | ||
const search = `! ${tagName}: `; | ||
const indexOfSearch = rule.indexOf(search); | ||
if (indexOfSearch >= 0) { | ||
return rule.substring(indexOfSearch + search.length).trim(); | ||
} | ||
} | ||
return null; | ||
}; | ||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; | ||
@@ -75,4 +48,4 @@ | ||
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({ | ||
__proto__: null, | ||
default: _nodeResolve_empty | ||
__proto__: null, | ||
default: _nodeResolve_empty | ||
}); | ||
@@ -713,3 +686,30 @@ | ||
// Lines of filter metadata to parse | ||
const AMOUNT_OF_LINES_TO_PARSE = 50; | ||
/** | ||
* Finds value of specified header tag in filter rules text. | ||
* | ||
* @param tagName Filter header tag name. | ||
* @param rules Lines of filter rules text. | ||
* | ||
* @returns Trimmed value of specified header tag or null if tag not found. | ||
*/ | ||
const parseTag = (tagName, rules) => { | ||
// Look up no more than 50 first lines | ||
const maxLines = Math.min(AMOUNT_OF_LINES_TO_PARSE, rules.length); | ||
for (let i = 0; i < maxLines; i += 1) { | ||
const rule = rules[i]; | ||
if (!rule) { | ||
continue; | ||
} | ||
const search = `! ${tagName}: `; | ||
const indexOfSearch = rule.indexOf(search); | ||
if (indexOfSearch >= 0) { | ||
return rule.substring(indexOfSearch + search.length).trim(); | ||
} | ||
} | ||
return null; | ||
}; | ||
/** | ||
* If the differential update is not available the server may signal about that | ||
@@ -904,3 +904,4 @@ * by returning one of the following responses. | ||
const filterLines = splitByLines(filterContent); | ||
const diffPath = parseTag(DIFF_PATH_TAG, filterLines); | ||
// Remove resourceName part after "#" sign if it exists. | ||
const diffPath = parseTag(DIFF_PATH_TAG, filterLines)?.split('#')[0]; | ||
const log = createLogger(verbose); | ||
@@ -907,0 +908,0 @@ if (!diffPath) { |
import axios from 'axios'; | ||
/** | ||
* Finds value of specified header tag in filter rules text. | ||
* | ||
* @param tagName Filter header tag name. | ||
* @param rules Lines of filter rules text. | ||
* | ||
* @returns Trimmed value of specified header tag or null if tag not found. | ||
*/ | ||
const parseTag = (tagName, rules) => { | ||
// Lines of filter metadata to parse | ||
const AMOUNT_OF_LINES_TO_PARSE = 50; | ||
// Look up no more than 50 first lines | ||
const maxLines = Math.min(AMOUNT_OF_LINES_TO_PARSE, rules.length); | ||
for (let i = 0; i < maxLines; i += 1) { | ||
const rule = rules[i]; | ||
if (!rule) { | ||
continue; | ||
} | ||
const search = `! ${tagName}: `; | ||
const indexOfSearch = rule.indexOf(search); | ||
if (indexOfSearch >= 0) { | ||
return rule.substring(indexOfSearch + search.length).trim(); | ||
} | ||
} | ||
return null; | ||
}; | ||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; | ||
@@ -72,4 +45,4 @@ | ||
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({ | ||
__proto__: null, | ||
default: _nodeResolve_empty | ||
__proto__: null, | ||
default: _nodeResolve_empty | ||
}); | ||
@@ -710,3 +683,30 @@ | ||
// Lines of filter metadata to parse | ||
const AMOUNT_OF_LINES_TO_PARSE = 50; | ||
/** | ||
* Finds value of specified header tag in filter rules text. | ||
* | ||
* @param tagName Filter header tag name. | ||
* @param rules Lines of filter rules text. | ||
* | ||
* @returns Trimmed value of specified header tag or null if tag not found. | ||
*/ | ||
const parseTag = (tagName, rules) => { | ||
// Look up no more than 50 first lines | ||
const maxLines = Math.min(AMOUNT_OF_LINES_TO_PARSE, rules.length); | ||
for (let i = 0; i < maxLines; i += 1) { | ||
const rule = rules[i]; | ||
if (!rule) { | ||
continue; | ||
} | ||
const search = `! ${tagName}: `; | ||
const indexOfSearch = rule.indexOf(search); | ||
if (indexOfSearch >= 0) { | ||
return rule.substring(indexOfSearch + search.length).trim(); | ||
} | ||
} | ||
return null; | ||
}; | ||
/** | ||
* If the differential update is not available the server may signal about that | ||
@@ -901,3 +901,4 @@ * by returning one of the following responses. | ||
const filterLines = splitByLines(filterContent); | ||
const diffPath = parseTag(DIFF_PATH_TAG, filterLines); | ||
// Remove resourceName part after "#" sign if it exists. | ||
const diffPath = parseTag(DIFF_PATH_TAG, filterLines)?.split('#')[0]; | ||
const log = createLogger(verbose); | ||
@@ -904,0 +905,0 @@ if (!diffPath) { |
@@ -43,3 +43,3 @@ /** | ||
* | ||
* @param oldFilterContent Old filter content. | ||
* @param oldDiffPathTag Diff-Path tag from old filter. | ||
* @param newFilterContent New filter content. | ||
@@ -50,3 +50,3 @@ * @param patchContent Patch content. | ||
*/ | ||
export declare const createDiffDirective: (oldFilterContent: string[], newFilterContent: string, patchContent: string) => string; | ||
export declare const createDiffDirective: (oldDiffPathTag: string | null, newFilterContent: string, patchContent: string) => string; | ||
/** | ||
@@ -53,0 +53,0 @@ * Parses a string to extract a Diff Directive object. |
@@ -17,5 +17,3 @@ import { TypesOfChanges } from '../common/types-of-change'; | ||
/** | ||
* The relative path to the directory where the patch should be saved. | ||
* The patch filename will be `<path_to_patches>/$PATCH_VERSION.patch`, | ||
* where `$PATCH_VERSION` is the value of `Version` from `<old_filter>`. | ||
* The relative path to the directory with patches. | ||
*/ | ||
@@ -74,51 +72,68 @@ patchesPath: string; | ||
/** | ||
* Updates the 'Diff-Path' tag value and recalculates the checksum, updating it | ||
* in the 'Checksum' tag of the new filter. This process ensures that changes | ||
* in these tags are reflected in the patch, allowing them to be applied | ||
* to the old filter. If the old and new filters are identical except for | ||
* these tags, the patch will contain only changes related | ||
* to 'Diff-Path' and 'Checksum'. | ||
* Checks if the provided file content contains a checksum tag within its first 200 characters. | ||
* This approach is selected to exclude parsing checksums from included filters. | ||
* | ||
* To verify the patch's significance, it's essential to check that it contains | ||
* more than just these tag updates. The checksum, typically on the first line, | ||
* is checked in the first three lines of the patch in RCS format. The next | ||
* three lines are examined for changes to the 'Diff-Path' tag, which can appear | ||
* anywhere in the new filter. | ||
* @param file The file content as a string. | ||
* | ||
* @param patch The patch array to be evaluated. | ||
* @returns Returns `true` if the patch contains only tag changes or is | ||
* otherwise empty, indicating no substantial differences between the filters. | ||
* Returns `false` if there are other changes. | ||
* @returns `true` if the checksum tag is found, otherwise `false`. | ||
*/ | ||
export declare const checkIfPatchIsEmpty: (patch: string) => boolean; | ||
export declare const hasChecksum: (file: string) => boolean; | ||
/** | ||
* Updates the 'Diff-Path' tag in a given filter array and recalculates the checksum. | ||
* The function first updates the 'Diff-Path' tag with the provided value, then removes | ||
* the existing checksum tag (if any), as the filter content has been altered. | ||
* It then calculates a new checksum for the updated filter and adds | ||
* this checksum tag to the filter. This process ensures that the filter's | ||
* metadata (Diff-Path and Checksum tags) accurately reflects its current content. | ||
* Updates the 'Diff-Path' tag and optionally recalculates and adds a new | ||
* checksum tag in a provided array of filter lines. | ||
* | ||
* @param filterToUpdate An array of strings representing the filter lines to be updated. | ||
* @param filterContent Filter content that needs to be updated. | ||
* @param diffPathTagValue The new value to be set for the 'Diff-Path' tag. | ||
* | ||
* @returns A new array of filter lines with the updated 'Diff-Path' tag and recalculated checksum. | ||
* @returns Updated filter content. | ||
*/ | ||
export declare const updateDiffPathInNewFilter: (filterToUpdate: string[], diffPathTagValue: string) => string[]; | ||
export declare const updateTags: (filterContent: string, diffPathTagValue: string) => string; | ||
/** | ||
* First verifies the version tags in the old and new filters, ensuring they are | ||
* present and in the correct order. Then calculates the difference between | ||
* the old and new filters in [RCS format](https://www.gnu.org/software/diffutils/manual/diffutils.html#RCS). | ||
* Optionally, calculates a diff directive with the name, checksum of new filter, | ||
* and line count, extracting the name from the `Diff-Name` tag in the old filter. | ||
* The resulting diff is saved to a patch file with the version number in the | ||
* specified path. Also updates the `Diff-Path` tag in the new filter. | ||
* Additionally, an empty patch file for the newer version is created. | ||
* Finally, scans the patch directory and deletes patches with the ".patch" | ||
* extension that have an mtime older than the specified threshold. | ||
* Determines if there are significant changes between two files, excluding | ||
* changes in 'Checksum' and 'Diff-Path' tags. | ||
* The function splits the file contents into lines, removes the mentioned tags, | ||
* and then compares the contents to determine if there are meaningful changes. | ||
* | ||
* TODO: Add tests for files operations. | ||
* @param oldFile The content of the old file as a string. | ||
* @param newFile The content of the new file as a string. | ||
* | ||
* @param params - Parameters for building the diff patch. | ||
* @returns `true` if there are significant changes, otherwise `false`. | ||
*/ | ||
export declare const hasChanges: (oldFile: string, newFile: string) => boolean; | ||
/** | ||
* Asynchronously updates the 'Diff-Path' tag in a new filter file and creates | ||
* a diff patch compared to an old file. | ||
* This function ensures that changes to 'Diff-Path' and 'Checksum' are correctly | ||
* included in the diff patch. | ||
* It throws an error if the old and new patch names are the same. | ||
* | ||
* @param oldFile The content of the old file as a string. | ||
* @param newFile The content of the new file as a string. | ||
* @param checksumFlag Flag to determine if a checksum should be added to the patch. | ||
* @param pathToPatchesRelativeToNewFilter The relative path to the patches directory from the new filter's location. | ||
* @param newFilePatchName The proposed diff name for the new file. | ||
* @param oldFilePatchName The diff name in the old file, or null if not present. | ||
* | ||
* @throws Error if the old and new patch names are the same. | ||
* | ||
* @returns A promise that resolves to an object containing the updated content | ||
* of the new file and the generated diff patch. | ||
*/ | ||
export declare const updateFileAndCreatePatch: (oldFile: string, newFile: string, checksumFlag: boolean, pathToPatchesRelativeToNewFilter: string, newFilePatchName: string, oldFilePatchName: string | null) => Promise<{ | ||
newFileWithUpdatedTags: string; | ||
patch: string; | ||
}>; | ||
/** | ||
* Asynchronously builds a diff between two filter files and handles related | ||
* file operations. Resolves paths, creates necessary folders, deletes outdated | ||
* patches, and checks for changes in filter content. | ||
* If there are changes other than those with 'Diff-Path' and 'Checksum' tags, | ||
* it updates the content of the new filter file with new 'Diff-Path' | ||
* and 'Checksum' tags and creates patch files accordingly. | ||
* | ||
* @param params The parameters including paths, resolution, and other settings | ||
* for diff generation. | ||
* | ||
* @returns A promise that resolves when the diff operation is complete. | ||
*/ | ||
export declare const buildDiff: (params: BuildDiffParams) => Promise<void>; |
@@ -11,2 +11,11 @@ /** | ||
/** | ||
* Finds value of specified header tag in filter rules text. | ||
* | ||
* @param tagName Filter header tag name. | ||
* @param rules Lines of filter rules text. | ||
* | ||
* @returns Trimmed value of specified header tag or null if tag not found. | ||
*/ | ||
export declare const parseTag: (tagName: string, rules: string[]) => string | null; | ||
/** | ||
* Removes a specified tag from an array of filter content strings. | ||
@@ -24,12 +33,1 @@ * This function searches for the first occurrence of the specified tag within | ||
export declare const removeTag: (tagName: string, filterContent: string[]) => string[]; | ||
/** | ||
* Finds tag by tag name in filter content and if found - updates with provided | ||
* value, if not - creates new tag and insert to first line of the filter. | ||
* | ||
* @param tagName Name of the tag. | ||
* @param tagValue Value of the tag. | ||
* @param filterContent Array of filter's rules. | ||
* | ||
* @returns Filter content with updated or created tag. | ||
*/ | ||
export declare const findAndUpdateTag: (tagName: string, tagValue: string, filterContent: string[]) => string[]; |
{ | ||
"name": "@adguard/diff-builder", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"description": "A tool for generating differential updates for filter lists.", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -27,3 +27,3 @@ # AdGuard Diff Builder | ||
- `<new_filter>` — the relative path to the new filter. | ||
- `<path_to_patches>` — the relative path to the directory where the patch should be saved. | ||
- `<path_to_patches>` — the relative path to the directory with patches. | ||
- `-n <name>` or `--name=<name>` — name of the patch file, an arbitrary string to identify the patch. | ||
@@ -53,47 +53,32 @@ Must be a string of length 1-64 with no spaces or other special characters. | ||
## Algorithm | ||
## Algorithm Overview | ||
### 1. Logging and File Path Resolution | ||
### 1. Setup | ||
- Resolve absolute paths for the old and new filters and the patches directory. | ||
- Create a logger for verbose output if `verbose` is `true`. | ||
- Resolve the absolute paths for `oldFilterPath`, `newFilterPath`, and `patchesPath`. | ||
### 2. Prepare Patch Directory | ||
- Ensure the patches directory exists, creating it if necessary. | ||
### 2. Read and Split Filter Contents | ||
### 3. Clean Up Old Patches | ||
- Delete any outdated patches from the patches directory. | ||
- Read the contents of `oldFilterPath` and `newFilterPath` into `oldFile` and `newFile`. | ||
- Determine the line endings for `oldFile` and `newFile`. | ||
- Split the contents of `oldFile` and `newFile` into arrays of lines (`oldFileSplitted` and `newFileSplitted`). | ||
### 4. Read Filters and Detect Changes | ||
- Read and split the old and new filter files into lines. | ||
- Check if there are significant changes between the two sets of lines, excluding 'Diff-Path' and 'Checksum' tags. | ||
### 3. Parse `Diff-Path` Tag | ||
### 5. Handle No Changes | ||
- If no significant changes are found, revert any changes in the new filter and exit. | ||
- Parse the `Diff-Path` tag from `oldFileSplitted` and store it in `oldFileDiffName`. | ||
### 6. Process Changes | ||
- Generate a new patch name and validate its uniqueness. | ||
- Update the 'Diff-Path' tag in the new filter. | ||
- Create a diff patch between the old and new filters. | ||
- Optionally, add a checksum to the patch. | ||
### 4. Create Patches Folder | ||
### 7. Finalize | ||
- Write the updated new filter back to its file. | ||
- Create an empty patch file for future use if necessary. | ||
- Save the diff patch to the appropriate file. | ||
- Create the `patchesPath` directory recursively if it doesn't exist. Log if it's created. | ||
### 5. Delete Outdated Patches | ||
- Scan `patchesPath` and delete outdated patches older than `deleteOlderThanSec`. Log the number of deleted patches. | ||
## 6. Check for File Sameness | ||
- Compare the checksums of `oldFile` and `newFile`. If they match, log and exit. | ||
## 7. Generate and Save the Diff | ||
- Generate a new patch name based on parameters. | ||
- Create an empty patch file for the new version if it doesn't exist. | ||
- Update the `Diff-Path` tag in `newFileSplitted`. | ||
- Calculate and save the difference between `oldFile` and `newFile` as a patch file. | ||
## 8. Log Patch File Path and Completion | ||
- Log the path where the patch file is saved. | ||
- The process is completed. | ||
## Important | ||
The `oldFilterPath` is expected to already contain a `Diff-Path` tag. | ||
## API | ||
@@ -100,0 +85,0 @@ |
@@ -20,7 +20,7 @@ #!/usr/bin/env node | ||
program | ||
/* eslint-disable max-len */ | ||
.command('build') | ||
.argument('<old_filter>', 'the relative path to the old filter') | ||
.argument('<new_filter>', 'the relative path to the new filter') | ||
/* eslint-disable max-len */ | ||
.argument('<path_to_patches>', 'the relative path to the directory where the patch should be saved. The patch filename will be `<path_to_patches>/$PATCH_VERSION.patch`, where `$PATCH_VERSION` is the value of `Version` from `<old_filter>`') | ||
.argument('<path_to_patches>', 'the relative path to the patches. The patch filename will be `<path_to_patches>/$PATCH`, where `$PATCH` is the "<patchName>[-<resolution>]-<epochTimestamp>-<expirationPeriod>.patch"') | ||
.requiredOption('-n, --name <name>', 'name of the patch file, an arbitrary string to identify the patch. Must be a string of length 1-64 with no spaces or other special characters.') | ||
@@ -27,0 +27,0 @@ .requiredOption('-t, --time <expirationPeriod>', 'expiration time for the diff update (the unit depends on `resolution`).') |
@@ -24,4 +24,2 @@ /** | ||
import { calculateChecksumSHA1 } from './calculate-checksum'; | ||
import { DIFF_PATH_TAG } from './constants'; | ||
import { parseTag } from './parse-tag'; | ||
@@ -57,3 +55,3 @@ const DIFF_DIRECTIVE = 'diff'; | ||
* | ||
* @param oldFilterContent Old filter content. | ||
* @param oldDiffPathTag Diff-Path tag from old filter. | ||
* @param newFilterContent New filter content. | ||
@@ -65,8 +63,7 @@ * @param patchContent Patch content. | ||
export const createDiffDirective = ( | ||
oldFilterContent: string[], | ||
oldDiffPathTag: string | null, | ||
newFilterContent: string, | ||
patchContent: string, | ||
): string => { | ||
const diffPath = parseTag(DIFF_PATH_TAG, oldFilterContent); | ||
const [, resourceName] = (diffPath || '').split('#'); | ||
const [, resourceName] = (oldDiffPathTag || '').split('#'); | ||
const checksum = calculateChecksumSHA1(newFilterContent); | ||
@@ -73,0 +70,0 @@ const lines = patchContent.split('\n').length - 1; |
@@ -158,12 +158,12 @@ /* eslint-disable jsdoc/require-description-complete-sentence */ | ||
const newFileDiffName = [name]; | ||
const newFilePatchName = [name]; | ||
if (resolution && resolution !== Resolution.Hours) { | ||
newFileDiffName.push(resolution); | ||
newFilePatchName.push(resolution); | ||
} | ||
newFileDiffName.push(epochTimestamp.toString()); | ||
newFileDiffName.push(time.toString()); | ||
newFilePatchName.push(epochTimestamp.toString()); | ||
newFilePatchName.push(time.toString()); | ||
return newFileDiffName.join('-').concat(FILE_EXTENSION); | ||
return newFilePatchName.join('-').concat(FILE_EXTENSION); | ||
}; | ||
@@ -170,0 +170,0 @@ |
@@ -6,11 +6,14 @@ import path from 'path'; | ||
import { parseTag } from '../common/parse-tag'; | ||
import { CHECKSUM_TAG, DIFF_PATH_TAG } from '../common/constants'; | ||
import { TypesOfChanges } from '../common/types-of-change'; | ||
import { createDiffDirective } from '../common/diff-directive'; | ||
import { calculateChecksumMD5, calculateChecksumSHA1 } from '../common/calculate-checksum'; | ||
import { calculateChecksumMD5 } from '../common/calculate-checksum'; | ||
import { Resolution, createPatchName } from '../common/patch-name'; | ||
import { splitByLines } from '../common/split-by-lines'; | ||
import { createLogger } from '../common/create-logger'; | ||
import { createTag, findAndUpdateTag, removeTag } from './tags'; | ||
import { | ||
createTag, | ||
parseTag, | ||
removeTag, | ||
} from './tags'; | ||
@@ -38,5 +41,3 @@ const DEFAULT_PATCH_TTL_SECONDS = 60 * 60 * 24 * 7; | ||
/** | ||
* The relative path to the directory where the patch should be saved. | ||
* The patch filename will be `<path_to_patches>/$PATCH_VERSION.patch`, | ||
* where `$PATCH_VERSION` is the value of `Version` from `<old_filter>`. | ||
* The relative path to the directory with patches. | ||
*/ | ||
@@ -220,6 +221,6 @@ patchesPath: string; | ||
/** | ||
* Scans `pathToPatches` for files with the "*.patch" pattern and deletes those | ||
* Scans `absolutePatchesPath` for files with the "*.patch" pattern and deletes those | ||
* whose `mtime` has expired. | ||
* | ||
* @param pathToPatches Directory for scan. | ||
* @param absolutePatchesPath Directory for scan. | ||
* @param deleteOlderThanSeconds The time to live for the patch in *seconds*. | ||
@@ -230,6 +231,6 @@ * | ||
const deleteOutdatedPatches = async ( | ||
pathToPatches: string, | ||
absolutePatchesPath: string, | ||
deleteOlderThanSeconds: number, | ||
): Promise<number> => { | ||
const files = await fs.promises.readdir(pathToPatches); | ||
const files = await fs.promises.readdir(absolutePatchesPath); | ||
const tasksToDeleteFiles: Promise<void>[] = []; | ||
@@ -241,3 +242,3 @@ for (const file of files) { | ||
const filePath = path.resolve(pathToPatches, file); | ||
const filePath = path.resolve(absolutePatchesPath, file); | ||
@@ -262,100 +263,155 @@ // eslint-disable-next-line no-await-in-loop | ||
/** | ||
* Updates the 'Diff-Path' tag value and recalculates the checksum, updating it | ||
* in the 'Checksum' tag of the new filter. This process ensures that changes | ||
* in these tags are reflected in the patch, allowing them to be applied | ||
* to the old filter. If the old and new filters are identical except for | ||
* these tags, the patch will contain only changes related | ||
* to 'Diff-Path' and 'Checksum'. | ||
* Checks if the provided file content contains a checksum tag within its first 200 characters. | ||
* This approach is selected to exclude parsing checksums from included filters. | ||
* | ||
* To verify the patch's significance, it's essential to check that it contains | ||
* more than just these tag updates. The checksum, typically on the first line, | ||
* is checked in the first three lines of the patch in RCS format. The next | ||
* three lines are examined for changes to the 'Diff-Path' tag, which can appear | ||
* anywhere in the new filter. | ||
* @param file The file content as a string. | ||
* | ||
* @param patch The patch array to be evaluated. | ||
* @returns Returns `true` if the patch contains only tag changes or is | ||
* otherwise empty, indicating no substantial differences between the filters. | ||
* Returns `false` if there are other changes. | ||
* @returns `true` if the checksum tag is found, otherwise `false`. | ||
*/ | ||
export const checkIfPatchIsEmpty = (patch: string): boolean => { | ||
const lines = splitByLines(patch); | ||
export const hasChecksum = (file: string): boolean => { | ||
const partOfFile = file.substring(0, 200); | ||
const lines = splitByLines(partOfFile); | ||
if (lines.length === 4 | ||
&& lines[0].startsWith('d1 2') | ||
&& lines[1].startsWith('a2 2') | ||
&& lines[2].startsWith(`! ${CHECKSUM_TAG}`) | ||
&& lines[3].startsWith(`! ${DIFF_PATH_TAG}`) | ||
) { | ||
return true; | ||
return lines.some((line) => line.startsWith(`! ${CHECKSUM_TAG}`)); | ||
}; | ||
/** | ||
* Updates the 'Diff-Path' tag and optionally recalculates and adds a new | ||
* checksum tag in a provided array of filter lines. | ||
* | ||
* @param filterContent Filter content that needs to be updated. | ||
* @param diffPathTagValue The new value to be set for the 'Diff-Path' tag. | ||
* | ||
* @returns Updated filter content. | ||
*/ | ||
export const updateTags = ( | ||
filterContent: string, | ||
diffPathTagValue: string, | ||
): string => { | ||
// Split the content of the filters into lines. | ||
let newFileSplitted = splitByLines(filterContent); | ||
// Remove tags 'Diff-Path' and 'Checksum' from new filterContent. | ||
newFileSplitted = removeTag(DIFF_PATH_TAG, removeTag(CHECKSUM_TAG, newFileSplitted)); | ||
const diffPath = createTag(DIFF_PATH_TAG, diffPathTagValue); | ||
newFileSplitted.unshift(diffPath); | ||
// If filter had checksum, calculate and insert a new checksum tag at the start of the filter | ||
if (hasChecksum(filterContent)) { | ||
const updatedChecksum = calculateChecksumMD5(newFileSplitted.join('')); | ||
const checksumTag = createTag(CHECKSUM_TAG, updatedChecksum); | ||
newFileSplitted.unshift(checksumTag); | ||
} | ||
if (lines.length === 6 | ||
&& lines[0].startsWith('d1 1') | ||
&& lines[1].startsWith('a1 1') | ||
&& lines[2].startsWith(`! ${CHECKSUM_TAG}`) | ||
&& lines[3].startsWith('d') | ||
&& lines[4].startsWith('a') | ||
&& lines[5].startsWith(`! ${DIFF_PATH_TAG}`) | ||
) { | ||
return true; | ||
return newFileSplitted.join(''); | ||
}; | ||
/** | ||
* Determines if there are significant changes between two files, excluding | ||
* changes in 'Checksum' and 'Diff-Path' tags. | ||
* The function splits the file contents into lines, removes the mentioned tags, | ||
* and then compares the contents to determine if there are meaningful changes. | ||
* | ||
* @param oldFile The content of the old file as a string. | ||
* @param newFile The content of the new file as a string. | ||
* | ||
* @returns `true` if there are significant changes, otherwise `false`. | ||
*/ | ||
export const hasChanges = ( | ||
oldFile: string, | ||
newFile: string, | ||
): boolean => { | ||
// Split the content of the filters into lines. | ||
let oldFileSplitted = splitByLines(oldFile); | ||
let newFileSplitted = splitByLines(newFile); | ||
// Remove 'Checksum' and 'Diff-Path' tags from both old and new filters. | ||
oldFileSplitted = removeTag(DIFF_PATH_TAG, removeTag(CHECKSUM_TAG, oldFileSplitted)); | ||
newFileSplitted = removeTag(DIFF_PATH_TAG, removeTag(CHECKSUM_TAG, newFileSplitted)); | ||
const oldFileHasChecksum = hasChecksum(oldFile); | ||
const newFileHasChecksum = hasChecksum(newFile); | ||
// Determine if there are meaningful changes in the files, excluding the 'Diff-Path' and 'Checksum' tags. | ||
// This comparison considers both the content and the presence of checksum tags in the old and new files. | ||
if (oldFileSplitted.join('') === newFileSplitted.join('') && oldFileHasChecksum === newFileHasChecksum) { | ||
return false; | ||
} | ||
return false; | ||
return true; | ||
}; | ||
/** | ||
* Updates the 'Diff-Path' tag in a given filter array and recalculates the checksum. | ||
* The function first updates the 'Diff-Path' tag with the provided value, then removes | ||
* the existing checksum tag (if any), as the filter content has been altered. | ||
* It then calculates a new checksum for the updated filter and adds | ||
* this checksum tag to the filter. This process ensures that the filter's | ||
* metadata (Diff-Path and Checksum tags) accurately reflects its current content. | ||
* Asynchronously updates the 'Diff-Path' tag in a new filter file and creates | ||
* a diff patch compared to an old file. | ||
* This function ensures that changes to 'Diff-Path' and 'Checksum' are correctly | ||
* included in the diff patch. | ||
* It throws an error if the old and new patch names are the same. | ||
* | ||
* @param filterToUpdate An array of strings representing the filter lines to be updated. | ||
* @param diffPathTagValue The new value to be set for the 'Diff-Path' tag. | ||
* @param oldFile The content of the old file as a string. | ||
* @param newFile The content of the new file as a string. | ||
* @param checksumFlag Flag to determine if a checksum should be added to the patch. | ||
* @param pathToPatchesRelativeToNewFilter The relative path to the patches directory from the new filter's location. | ||
* @param newFilePatchName The proposed diff name for the new file. | ||
* @param oldFilePatchName The diff name in the old file, or null if not present. | ||
* | ||
* @returns A new array of filter lines with the updated 'Diff-Path' tag and recalculated checksum. | ||
* @throws Error if the old and new patch names are the same. | ||
* | ||
* @returns A promise that resolves to an object containing the updated content | ||
* of the new file and the generated diff patch. | ||
*/ | ||
export const updateDiffPathInNewFilter = ( | ||
filterToUpdate: string[], | ||
diffPathTagValue: string, | ||
): string[] => { | ||
// Make a copy | ||
let updatedFilter = filterToUpdate.slice(); | ||
export const updateFileAndCreatePatch = async ( | ||
oldFile: string, | ||
newFile: string, | ||
checksumFlag: boolean, | ||
pathToPatchesRelativeToNewFilter: string, | ||
newFilePatchName: string, | ||
oldFilePatchName: string | null, | ||
): Promise<{ | ||
newFileWithUpdatedTags: string, | ||
patch: string, | ||
}> => { | ||
// Verify that the patch names are not the same. | ||
if (oldFilePatchName === newFilePatchName) { | ||
// eslint-disable-next-line max-len | ||
throw new Error(`The old patch name "${oldFilePatchName}" and the new patch name "${newFilePatchName}" are the same. Consider changing the unit of measure or waiting.`); | ||
} | ||
updatedFilter = findAndUpdateTag( | ||
DIFF_PATH_TAG, | ||
diffPathTagValue, | ||
updatedFilter, | ||
// Note: Update 'Diff-Path' and 'Checksum' before calculating the diff | ||
// to ensure their changes are included in the resulting diff patch. | ||
const newFilterDiffPathTagValue = path.join(pathToPatchesRelativeToNewFilter, newFilePatchName); | ||
const newFileWithUpdatedTags = updateTags( | ||
newFile, | ||
newFilterDiffPathTagValue, | ||
); | ||
// Remove first found checksum tag, because we changed filter's content | ||
// via adding Diff-Path tag, so we need to recalculate checksum. | ||
updatedFilter = removeTag(CHECKSUM_TAG, updatedFilter); | ||
// Generate the diff patch. | ||
let patch = createPatch(oldFile, newFileWithUpdatedTags); | ||
// Calculate checksum for new filter and insert it in the filter | ||
// to the first line. | ||
const updatedChecksum = calculateChecksumMD5(updatedFilter.join('')); | ||
const checksumTag = createTag(CHECKSUM_TAG, updatedChecksum); | ||
updatedFilter.unshift(checksumTag); | ||
// Optionally add a checksum to the patch. | ||
if (checksumFlag) { | ||
const diffDirective = createDiffDirective(oldFilePatchName, newFileWithUpdatedTags, patch); | ||
patch = diffDirective.concat('\n', patch); | ||
} | ||
return updatedFilter; | ||
return { | ||
newFileWithUpdatedTags, | ||
patch, | ||
}; | ||
}; | ||
/** | ||
* First verifies the version tags in the old and new filters, ensuring they are | ||
* present and in the correct order. Then calculates the difference between | ||
* the old and new filters in [RCS format](https://www.gnu.org/software/diffutils/manual/diffutils.html#RCS). | ||
* Optionally, calculates a diff directive with the name, checksum of new filter, | ||
* and line count, extracting the name from the `Diff-Name` tag in the old filter. | ||
* The resulting diff is saved to a patch file with the version number in the | ||
* specified path. Also updates the `Diff-Path` tag in the new filter. | ||
* Additionally, an empty patch file for the newer version is created. | ||
* Finally, scans the patch directory and deletes patches with the ".patch" | ||
* extension that have an mtime older than the specified threshold. | ||
* Asynchronously builds a diff between two filter files and handles related | ||
* file operations. Resolves paths, creates necessary folders, deletes outdated | ||
* patches, and checks for changes in filter content. | ||
* If there are changes other than those with 'Diff-Path' and 'Checksum' tags, | ||
* it updates the content of the new filter file with new 'Diff-Path' | ||
* and 'Checksum' tags and creates patch files accordingly. | ||
* | ||
* TODO: Add tests for files operations. | ||
* @param params The parameters including paths, resolution, and other settings | ||
* for diff generation. | ||
* | ||
* @param params - Parameters for building the diff patch. | ||
* @returns A promise that resolves when the diff operation is complete. | ||
*/ | ||
@@ -370,3 +426,3 @@ export const buildDiff = async (params: BuildDiffParams): Promise<void> => { | ||
resolution = Resolution.Hours, | ||
checksum = false, | ||
checksum: checksumFlag = false, | ||
deleteOlderThanSec = DEFAULT_PATCH_TTL_SECONDS, | ||
@@ -378,109 +434,92 @@ verbose = false, | ||
// Paths | ||
const prevListPath = path.resolve(process.cwd(), oldFilterPath); | ||
const newListPath = path.resolve(process.cwd(), newFilterPath); | ||
const pathToPatches = path.resolve(process.cwd(), patchesPath); | ||
// Resolve all necessary paths. | ||
const absoluteOldListPath = path.resolve(process.cwd(), oldFilterPath); | ||
const absoluteNewListPath = path.resolve(process.cwd(), newFilterPath); | ||
const absolutePatchesPath = path.resolve(process.cwd(), patchesPath); | ||
const pathToPatchesRelativeToNewFilter = path.relative( | ||
path.dirname(newFilterPath), | ||
absolutePatchesPath, | ||
); | ||
// Filters' content | ||
const oldFile = await fs.promises.readFile(prevListPath, { encoding: 'utf-8' }); | ||
let newFile = await fs.promises.readFile(newListPath, { encoding: 'utf-8' }); | ||
log(`Checking diff between "${absoluteOldListPath}" and "${absoluteNewListPath}".`); | ||
log(`Path to patches: "${absolutePatchesPath}".`); | ||
// Splitted filters' content | ||
const oldFileSplitted = splitByLines(oldFile); | ||
let newFileSplitted = splitByLines(newFile); | ||
const oldFileDiffName = parseTag(DIFF_PATH_TAG, oldFileSplitted); | ||
// Create folder for patches if it doesn't exists. | ||
if (!fs.existsSync(pathToPatches)) { | ||
await fs.promises.mkdir(pathToPatches, { recursive: true }); | ||
log(`Folder for patches does not exists, created at '${pathToPatches}'.`); | ||
// Create the patches folder if it doesn't exist. | ||
if (!fs.existsSync(absolutePatchesPath)) { | ||
await fs.promises.mkdir(absolutePatchesPath, { recursive: true }); | ||
log(`Created missing patches folder at "${absolutePatchesPath}".`); | ||
} | ||
// Scan patches folder and delete outdated patches. | ||
// Scan the patches folder and delete outdated patches. | ||
const deleted = await deleteOutdatedPatches( | ||
pathToPatches, | ||
absolutePatchesPath, | ||
deleteOlderThanSec, | ||
); | ||
log(`Deleted outdated patches: ${deleted}`); | ||
// If files are the same and old filter already has diff-path - there are nothing to do. | ||
// Otherwise, create empty patch for future version and exit. | ||
if (calculateChecksumSHA1(oldFile) === calculateChecksumSHA1(newFile) && oldFileDiffName) { | ||
log('Files are the same. Nothing to do.'); | ||
return; | ||
if (deleted > 0) { | ||
log(`Deleted ${deleted} outdated patches from "${absolutePatchesPath}".`); | ||
} | ||
// Generate name for new patch | ||
const newFileDiffName = createPatchName({ name, resolution, time }); | ||
// Read the content of the filters. | ||
const oldFile = await fs.promises.readFile(absoluteOldListPath, { encoding: 'utf-8' }); | ||
const newFile = await fs.promises.readFile(absoluteNewListPath, { encoding: 'utf-8' }); | ||
if (oldFileDiffName === newFileDiffName) { | ||
// eslint-disable-next-line max-len | ||
throw new Error(`Old patch name "${oldFileDiffName}" and new patch name "${newFileDiffName}" are the same. Change the unit of measure to a smaller one or wait.`); | ||
} | ||
// Check for any changes except changes with Diff-Path and Checksum | ||
// in the filters. | ||
if (!hasChanges(oldFile, newFile)) { | ||
// If no significant changes, undo removal of 'Diff-Path' (it happens | ||
// by run `compiler` which currently not supported `Diff-Path` tag and | ||
// always remove it, even if filter has not changes) | ||
// and save the old file content to the new file. | ||
await fs.promises.writeFile(absoluteNewListPath, oldFile); | ||
// Create empty patch for future version if it doesn't exists. | ||
const emptyPatchForNewVersion = path.join(pathToPatches, newFileDiffName); | ||
if (!fs.existsSync(emptyPatchForNewVersion)) { | ||
await fs.promises.writeFile(emptyPatchForNewVersion, ''); | ||
log(`Created patch for new filter at ${emptyPatchForNewVersion}.`); | ||
log('No significant changes found.'); | ||
log(`Reverted any removal of 'Diff-Path' in the new filter "${absoluteNewListPath}".`); | ||
return; | ||
} | ||
// Note: Update `Diff-Path` and 'Checksum' before calculating the diff | ||
// to ensure that changing `Diff-Path` and 'Checksum' will be correctly | ||
// included in the resulting diff patch. | ||
const pathToPatchesRelativeToNewFilter = path.relative( | ||
path.dirname(newFilterPath), | ||
pathToPatches, | ||
); | ||
const newFilterDiffPathTagValue = path.join(pathToPatchesRelativeToNewFilter, newFileDiffName); | ||
newFileSplitted = updateDiffPathInNewFilter( | ||
newFileSplitted, | ||
newFilterDiffPathTagValue, | ||
); | ||
newFile = newFileSplitted.join(''); | ||
// Retrieve and save the 'Diff-Path' tag from the old filter before removal. | ||
let oldFilePatchName = parseTag(DIFF_PATH_TAG, splitByLines(oldFile)); | ||
// Remove resourceName part after "#" sign if it exists. | ||
oldFilePatchName = oldFilePatchName ? oldFilePatchName.split('#')[0] : null; | ||
// Because we already created empty patch for new version, we also need to | ||
// update `Diff-Path` (which contains path to created empty patch) and | ||
// `Checksum` tags in the new filter. | ||
await fs.promises.writeFile( | ||
newListPath, | ||
// Generate a name for the new patch. | ||
const newFilePatchName = createPatchName({ name, resolution, time }); | ||
const { | ||
newFileWithUpdatedTags, | ||
patch, | ||
} = await updateFileAndCreatePatch( | ||
oldFile, | ||
newFile, | ||
checksumFlag, | ||
pathToPatchesRelativeToNewFilter, | ||
newFilePatchName, | ||
oldFilePatchName, | ||
); | ||
// We cannot save diff, if diff in old file doesn't exists. | ||
if (!oldFileDiffName) { | ||
log('Not found "Diff-Path" in the old filter. Patch for old file can not be created.'); | ||
return; | ||
// Write the updated content to the new filter with an updated 'Diff-Path' and 'Checksum'. | ||
await fs.promises.writeFile(absoluteNewListPath, newFileWithUpdatedTags); | ||
log(`Updated 'Diff-Path' and 'Checksum' tags in the new filter at "${absoluteNewListPath}".`); | ||
// Create an empty patch for the future version if it doesn't exist. | ||
const emptyPatchForNewVersion = path.join(absolutePatchesPath, newFilePatchName); | ||
if (!fs.existsSync(emptyPatchForNewVersion)) { | ||
await fs.promises.writeFile(emptyPatchForNewVersion, ''); | ||
log(`Created a patch for the new filter at ${emptyPatchForNewVersion}.`); | ||
} | ||
// Calculate diff | ||
let patch = createPatch(oldFile, newFile); | ||
// If patch is empty - don't write it to file. | ||
if (checkIfPatchIsEmpty(patch)) { | ||
log('No changes detected between old and new files. Patch would not be created.'); | ||
// If 'Diff-Path' is not found in the old filter, a patch for the old file | ||
// cannot be created. | ||
if (!oldFilePatchName) { | ||
log('No "Diff-Path" found in the old filter. Cannot create a patch for the old file.'); | ||
return; | ||
} | ||
// Add checksum to patch if requested | ||
if (checksum) { | ||
const diffDirective = createDiffDirective(oldFileSplitted, newFile, patch); | ||
patch = diffDirective.concat('\n', patch); | ||
} | ||
// Diff-Path contains path relative to the filter path, so we need | ||
// to resolve path. | ||
const oldFilePatch = path.resolve( | ||
path.dirname(prevListPath), | ||
oldFileDiffName, | ||
); | ||
// Save diff to patch file. | ||
await fs.promises.writeFile( | ||
oldFilePatch, | ||
patch, | ||
); | ||
log(`Wrote patch to: ${oldFilePatch}`); | ||
// 'Diff-Path' contains a path relative to the filter path, requiring path resolution. | ||
const oldFilePatch = path.resolve(path.dirname(absoluteOldListPath), oldFilePatchName); | ||
// Save the diff to the patch file. | ||
await fs.promises.writeFile(oldFilePatch, patch); | ||
log(`Saved the patch to: ${oldFilePatch}`); | ||
}; |
@@ -1,2 +0,3 @@ | ||
import { parseTag } from '../common/parse-tag'; | ||
// Lines of filter metadata to parse | ||
const AMOUNT_OF_LINES_TO_PARSE = 50; | ||
@@ -16,2 +17,31 @@ /** | ||
/** | ||
* Finds value of specified header tag in filter rules text. | ||
* | ||
* @param tagName Filter header tag name. | ||
* @param rules Lines of filter rules text. | ||
* | ||
* @returns Trimmed value of specified header tag or null if tag not found. | ||
*/ | ||
export const parseTag = (tagName: string, rules: string[]): string | null => { | ||
// Look up no more than 50 first lines | ||
const maxLines = Math.min(AMOUNT_OF_LINES_TO_PARSE, rules.length); | ||
for (let i = 0; i < maxLines; i += 1) { | ||
const rule = rules[i]; | ||
if (!rule) { | ||
continue; | ||
} | ||
const search = `! ${tagName}: `; | ||
const indexOfSearch = rule.indexOf(search); | ||
if (indexOfSearch >= 0) { | ||
return rule.substring(indexOfSearch + search.length).trim(); | ||
} | ||
} | ||
return null; | ||
}; | ||
/** | ||
* Removes a specified tag from an array of filter content strings. | ||
@@ -33,3 +63,6 @@ * This function searches for the first occurrence of the specified tag within | ||
// Make copy | ||
const updatedFile = filterContent.slice(); | ||
const updatedFile = filterContent.slice( | ||
0, | ||
Math.min(AMOUNT_OF_LINES_TO_PARSE, filterContent.length), | ||
); | ||
@@ -44,30 +77,1 @@ const tagIdx = updatedFile.findIndex((line) => line.includes(tagName)); | ||
}; | ||
/** | ||
* Finds tag by tag name in filter content and if found - updates with provided | ||
* value, if not - creates new tag and insert to first line of the filter. | ||
* | ||
* @param tagName Name of the tag. | ||
* @param tagValue Value of the tag. | ||
* @param filterContent Array of filter's rules. | ||
* | ||
* @returns Filter content with updated or created tag. | ||
*/ | ||
export const findAndUpdateTag = ( | ||
tagName: string, | ||
tagValue: string, | ||
filterContent: string[], | ||
): string[] => { | ||
// Make copy | ||
const updatedFile = filterContent.slice(); | ||
const updatedTag = createTag(tagName, tagValue); | ||
if (parseTag(tagName, updatedFile)) { | ||
const tagIdx = updatedFile.findIndex((line) => line.includes(tagName)); | ||
updatedFile[tagIdx] = updatedTag; | ||
} else { | ||
updatedFile.unshift(updatedTag); | ||
} | ||
return updatedFile; | ||
}; |
import axios from 'axios'; | ||
import { parseTag } from '../common/parse-tag'; | ||
import { calculateChecksumSHA1 } from '../common/calculate-checksum'; | ||
@@ -12,2 +11,3 @@ import { DIFF_PATH_TAG } from '../common/constants'; | ||
import { getErrorMessage } from '../common/get-error-message'; | ||
import { parseTag } from '../diff-builder/tags'; | ||
@@ -304,4 +304,6 @@ /** | ||
const filterLines = splitByLines(filterContent); | ||
const diffPath = parseTag(DIFF_PATH_TAG, filterLines); | ||
// Remove resourceName part after "#" sign if it exists. | ||
const diffPath = parseTag(DIFF_PATH_TAG, filterLines)?.split('#')[0]; | ||
const log = createLogger(verbose); | ||
@@ -308,0 +310,0 @@ |
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
348999
7092
35
129