summaryrefslogtreecommitdiffstats
path: root/uAssets/tools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /uAssets/tools
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'uAssets/tools')
-rwxr-xr-xuAssets/tools/make-diffpatch.sh100
-rw-r--r--uAssets/tools/make-easylist.mjs201
-rwxr-xr-xuAssets/tools/make-easylist.sh35
-rwxr-xr-xuAssets/tools/make-ublock.sh21
-rwxr-xr-xuAssets/tools/make-validate.sh12
-rw-r--r--uAssets/tools/need-patch.mjs81
-rwxr-xr-xuAssets/tools/update-3rdparties.sh30
-rwxr-xr-xuAssets/tools/update-diffpatches.sh115
-rw-r--r--uAssets/tools/validate/config.js33
-rw-r--r--uAssets/tools/validate/package.json6
-rw-r--r--uAssets/tools/validate/validate.js321
11 files changed, 955 insertions, 0 deletions
diff --git a/uAssets/tools/make-diffpatch.sh b/uAssets/tools/make-diffpatch.sh
new file mode 100755
index 0000000..81e0aab
--- /dev/null
+++ b/uAssets/tools/make-diffpatch.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+#
+# This script assumes a linux environment
+
+set -e
+
+# To be executed at the root of CDN repo
+#
+# It's not being hosted at CDN because that
+# repo is also used as a website
+
+VERSION=$1
+if [[ -z $VERSION ]]; then
+ echo "Error: No version provided, aborting"
+ exit 1
+fi
+
+PATCHES_DIR=$2
+if [[ -z $PATCHES_DIR ]]; then
+ echo "Error: patches directory is not provided, aborting"
+ exit 1
+fi
+
+PREVIOUS_VERSION=$(<version)
+PREVIOUS_PATCH_FILE="$PATCHES_DIR/$PREVIOUS_VERSION.patch"
+: > "$PREVIOUS_PATCH_FILE"
+
+NEXT_PATCH_FILE="$PATCHES_DIR/$VERSION.patch"
+
+# Temporary file to receive the RCS patch data
+DIFF=$(mktemp)
+
+FILES=( $(git diff --name-only) )
+for FILE in "${FILES[@]}"; do
+
+ # Reference:
+ # https://github.com/ameshkov/diffupdates
+
+ if (head "$FILE" | grep -q '^! Version: '); then
+ sed -Ei "1,10s;^! Version: .+$;! Version: $VERSION;" "$FILE"
+ fi
+
+ # Patches are for filter lists supporting differential updates
+ if (head "$FILE" | grep -q '^! Diff-Path: '); then
+
+ # Extract diff name from `! Diff-Path:` field
+ DIFF_NAME=$(grep -m 1 -oP '^! Diff-Path: [^#]+#?\K.*' "$FILE")
+ # Fall back to `! Diff-Name:` field if no name found
+ # Remove once `! Diff-Name:` is no longer needed after transition
+ if [[ -z $DIFF_NAME ]]; then
+ DIFF_NAME=$(grep -m 1 -oP '^! Diff-Name: \K.+' "$FILE")
+ fi
+ echo "Info: Diff name for ${FILE} is ${DIFF_NAME}"
+
+ # We need a patch name to generate a valid patch
+ if [[ -n $DIFF_NAME ]]; then
+
+ # Compute relative patch path
+ PATCH_PATH="$(realpath --relative-to="$(dirname "$FILE")" "$NEXT_PATCH_FILE")"
+
+ # Fill in patch path to next version (do not clobber hash portion)
+ sed -Ei "1,10s;^! Diff-Path: [^#]+(#.+)?$;! Diff-Path: $PATCH_PATH\1;" "$FILE"
+
+ # Compute the RCS diff between current version and new version
+ git show "HEAD:$FILE" | diff -n - "$FILE" > "$DIFF" || true
+
+ FILE_CHECKSUM="$(sha1sum "$FILE")"
+ FILE_CHECKSUM=${FILE_CHECKSUM:0:10}
+
+ DIFF_LINES=$(wc -l < "$DIFF")
+ echo "Info: Computed patch for ${FILE} has ${DIFF_LINES} lines"
+
+ # Populate output file with patch information
+ echo "Info: Adding patch data of ${FILE} to ${PREVIOUS_PATCH_FILE}"
+ echo "diff name:$DIFF_NAME lines:$DIFF_LINES checksum:$FILE_CHECKSUM" >> "$PREVIOUS_PATCH_FILE"
+ cat "$DIFF" >> "$PREVIOUS_PATCH_FILE"
+
+ else
+
+ echo "Error: Diff name not found, skipping"
+
+ fi
+ fi
+
+ # Stage changed file
+ echo "Info: Staging $FILE"
+ git add -u "$FILE"
+
+done
+
+# Create a patch only if there was a previous version
+if [[ -n $PREVIOUS_VERSION ]]; then
+ echo "Info: Staging $PREVIOUS_PATCH_FILE"
+ git add "$PREVIOUS_PATCH_FILE"
+fi
+
+echo -n "$VERSION" > version
+git add version
+
+rm -f "$DIFF"
diff --git a/uAssets/tools/make-easylist.mjs b/uAssets/tools/make-easylist.mjs
new file mode 100644
index 0000000..b648363
--- /dev/null
+++ b/uAssets/tools/make-easylist.mjs
@@ -0,0 +1,201 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2022-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+// jshint node:true, esversion:9
+
+'use strict';
+
+/******************************************************************************/
+
+import fs from 'fs/promises';
+import path from 'path';
+import process from 'process';
+
+/******************************************************************************/
+
+const expandedParts = new Set();
+
+/******************************************************************************/
+
+const commandLineArgs = (( ) => {
+ const args = new Map();
+ let name, value;
+ for ( const arg of process.argv.slice(2) ) {
+ const pos = arg.indexOf('=');
+ if ( pos === -1 ) {
+ name = arg;
+ value = '';
+ } else {
+ name = arg.slice(0, pos);
+ value = arg.slice(pos+1);
+ }
+ args.set(name, value);
+ }
+ return args;
+})();
+
+/******************************************************************************/
+
+function expandTemplate(wd, parts) {
+ const out = [];
+ const reInclude = /^%include +(.+):(.+)%\s+/gm;
+ const trim = text => trimSublist(text);
+ for ( const part of parts ) {
+ if ( typeof part !== 'string' ) {
+ out.push(part);
+ continue;
+ }
+ let lastIndex = 0;
+ for (;;) {
+ const match = reInclude.exec(part);
+ if ( match === null ) { break; }
+ out.push(part.slice(lastIndex, match.index).trim());
+ const repo = match[1].trim();
+ const fpath = `${match[2].trim()}`;
+ if ( expandedParts.has(fpath) === false ) {
+ console.info(` Inserting ${fpath}`);
+ out.push(
+ out.push({ file: `${fpath}` }),
+ `! *** ${repo}:${fpath} ***`,
+ fs.readFile(`${wd}/${fpath}`, { encoding: 'utf8' })
+ .then(text => fpath.includes('header') ? text : trim(text)),
+ );
+ expandedParts.add(fpath);
+ }
+ lastIndex = reInclude.lastIndex;
+ }
+ out.push(part.slice(lastIndex).trim());
+ }
+ return out;
+}
+
+/******************************************************************************/
+
+function expandIncludeDirectives(wd, parts) {
+ const out = [];
+ const reInclude = /^!#include (.+)\s*/gm;
+ const trim = text => trimSublist(text);
+ let parentPath = '';
+ for ( const part of parts ) {
+ if ( typeof part !== 'string' ) {
+ if ( typeof part === 'object' && part.file !== undefined ) {
+ parentPath = part.file;
+ }
+ out.push(part);
+ continue;
+ }
+ let lastIndex = 0;
+ for (;;) {
+ const match = reInclude.exec(part);
+ if ( match === null ) { break; }
+ out.push(part.slice(lastIndex, match.index).trim());
+ const fpath = `${path.dirname(parentPath)}/${match[1].trim()}`;
+ if ( expandedParts.has(fpath) === false ) {
+ console.info(` Inserting ${fpath}`);
+ out.push(
+ { file: fpath },
+ `! *** ${fpath} ***`,
+ fs.readFile(`${wd}/${fpath}`, { encoding: 'utf8' })
+ .then(text => fpath.includes('header') ? text : trim(text)),
+ );
+ expandedParts.add(fpath);
+ }
+ lastIndex = reInclude.lastIndex;
+ }
+ out.push(part.slice(lastIndex).trim());
+ }
+ return out;
+}
+
+/******************************************************************************/
+
+function trimSublist(text) {
+ // Remove empty comment lines
+ text = text.replace(/^!\s*$(?:\r\n|\n)/gm, '');
+ // Remove sublist header information: the importing list will provide its
+ // own header.
+ text = text.trim().replace(/^(?:!\s+[^\r\n]+?(?:\r\n|\n))+/s, '');
+ return text;
+}
+
+/******************************************************************************/
+
+function minify(text) {
+ // remove issue-related comments
+ text = text.replace(/^! https:\/\/.*?[\n\r]+/gm, '');
+ // remove empty lines
+ text = text.replace(/^[\n\r]+/gm, '');
+ // convert potentially present Windows-style newlines
+ text = text.replace(/\r\n/g, '\n');
+ return text;
+}
+
+/******************************************************************************/
+
+function assemble(parts) {
+ const out = [];
+ for ( const part of parts ) {
+ if ( typeof part !== 'string' ) { continue; }
+ out.push(part);
+ }
+ return out.join('\n').trim() + '\n';
+}
+
+/******************************************************************************/
+
+async function main() {
+ const workingDir = commandLineArgs.get('dir') || '.';
+ const inFile = commandLineArgs.get('in');
+ if ( typeof inFile !== 'string' || inFile === '' ) {
+ process.exit(1);
+ }
+ const outFile = commandLineArgs.get('out');
+ if ( typeof outFile !== 'string' || outFile === '' ) {
+ process.exit(1);
+ }
+
+ console.info(` Using template at ${inFile}`);
+
+ const inText = fs.readFile(`${workingDir}/${inFile}`, { encoding: 'utf8' });
+
+ let parts = [ inText ];
+ do {
+ parts = await Promise.all(parts);
+ parts = expandTemplate(workingDir, parts);
+ } while ( parts.some(v => v instanceof Promise) );
+
+ do {
+ parts = await Promise.all(parts);
+ parts = expandIncludeDirectives(workingDir, parts);
+ } while ( parts.some(v => v instanceof Promise));
+
+ let afterText = assemble(parts);
+
+ if ( commandLineArgs.get('minify') !== undefined ) {
+ afterText = minify(afterText);
+ }
+
+ console.info(` Creating ${outFile}`);
+
+ fs.writeFile(outFile, afterText);
+}
+
+main();
diff --git a/uAssets/tools/make-easylist.sh b/uAssets/tools/make-easylist.sh
new file mode 100755
index 0000000..0185d8a
--- /dev/null
+++ b/uAssets/tools/make-easylist.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+#
+# This script assumes a linux environment
+
+echo "*** uAssets: Assembling EasyList lists"
+TMPDIR=$(mktemp -d)
+mkdir -p $TMPDIR/easylist
+git clone --depth 1 https://github.com/easylist/easylist.git $TMPDIR/easylist
+cp -R templates/easy*.template $TMPDIR/easylist/
+
+echo "*** uAssets: Assembling easylist.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist.template out=thirdparties/easylist/easylist.txt
+
+echo "*** uAssets: Assembling easyprivacy.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easyprivacy.template out=thirdparties/easylist/easyprivacy.txt
+
+echo "*** uAssets: Assembling easylist-annoyances.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-annoyances.template out=thirdparties/easylist/easylist-annoyances.txt
+
+echo "*** uAssets: Assembling easylist-cookies.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-cookies.template out=thirdparties/easylist/easylist-cookies.txt
+
+echo "*** uAssets: Assembling easylist-social.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-social.template out=thirdparties/easylist/easylist-social.txt
+
+echo "*** uAssets: Assembling easylist-newsletters.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-newsletters.template out=thirdparties/easylist/easylist-newsletters.txt
+
+echo "*** uAssets: Assembling easylist-notifications.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-notifications.template out=thirdparties/easylist/easylist-notifications.txt
+
+echo "*** uAssets: Assembling easylist-chat.txt"
+node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-chat.template out=thirdparties/easylist/easylist-chat.txt
+
+rm -rf $TMPDIR
diff --git a/uAssets/tools/make-ublock.sh b/uAssets/tools/make-ublock.sh
new file mode 100755
index 0000000..f35fb13
--- /dev/null
+++ b/uAssets/tools/make-ublock.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+#
+# This script assumes a linux environment
+
+echo "*** uAssets: Assembling filters/filters.txt"
+node ./tools/make-easylist.mjs in=templates/ublock-filters.template out=filters/filters.min.txt minify=1
+
+echo "*** uAssets: Assembling filters/quick-fixes.txt"
+node ./tools/make-easylist.mjs in=templates/ublock-quick-fixes.template out=filters/quick-fixes.min.txt minify=1
+
+echo "*** uAssets: Assembling filters/privacy.txt"
+node ./tools/make-easylist.mjs in=templates/ublock-privacy.template out=filters/privacy.min.txt minify=1
+
+echo "*** uAssets: Assembling filters/unbreak.txt"
+node ./tools/make-easylist.mjs in=templates/ublock-unbreak.template out=filters/unbreak.min.txt minify=1
+
+echo "*** uAssets: Assembling filters/badware.txt"
+node ./tools/make-easylist.mjs in=templates/ublock-badware.template out=filters/badware.min.txt minify=1
+
+echo "*** uAssets: Assembling filters/annoyances.txt"
+node ./tools/make-easylist.mjs in=templates/ublock-annoyances.template out=filters/annoyances.min.txt minify=1
diff --git a/uAssets/tools/make-validate.sh b/uAssets/tools/make-validate.sh
new file mode 100755
index 0000000..8d7ff9b
--- /dev/null
+++ b/uAssets/tools/make-validate.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+#
+# This script assumes a linux environment
+
+# https://stackoverflow.com/a/52526704
+echo "*** Importing required uBO files"
+mkdir -p build/validate
+git clone --filter=blob:none --no-checkout https://github.com/gorhill/uBlock.git build/validate/uBlock
+cd build/validate/uBlock
+git sparse-checkout init --cone
+git sparse-checkout set src/js src/lib
+cd -
diff --git a/uAssets/tools/need-patch.mjs b/uAssets/tools/need-patch.mjs
new file mode 100644
index 0000000..c0cf231
--- /dev/null
+++ b/uAssets/tools/need-patch.mjs
@@ -0,0 +1,81 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2023-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+// jshint node:true, esversion:9
+
+'use strict';
+
+/******************************************************************************/
+
+import fs from 'fs/promises';
+import path from 'path';
+import process from 'process';
+
+/******************************************************************************/
+
+const commandLineArgs = (( ) => {
+ const args = new Map();
+ let name, value;
+ for ( const arg of process.argv.slice(2) ) {
+ const pos = arg.indexOf('=');
+ if ( pos === -1 ) {
+ name = arg;
+ value = '';
+ } else {
+ name = arg.slice(0, pos);
+ value = arg.slice(pos+1);
+ }
+ args.set(name, value);
+ }
+ return args;
+})();
+
+/******************************************************************************/
+
+async function main() {
+ const MS_PER_HOUR = 60 * 60 * 1000;
+ const targetDelayInHours = parseInt(commandLineArgs.get('delay') || '5', 10);
+
+ const hoursSinceEpoch = Math.floor(Date.now() / MS_PER_HOUR);
+ if ( (hoursSinceEpoch % targetDelayInHours) === 0 ) {
+ console.log('yes');
+ process.exit(0);
+ }
+
+ const version = await fs.readFile('version', { encoding: 'utf8' });
+ const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(version);
+ if ( match === null ) {
+ console.log('yes');
+ process.exit(0);
+ }
+
+ const date = new Date();
+ date.setUTCFullYear(
+ parseInt(match[1], 10),
+ parseInt(match[2], 10) - 1,
+ parseInt(match[3], 10)
+ );
+ date.setUTCHours(0, parseInt(match[4], 10), 0, 0);
+ const expiredTimeInHours = (Date.now() - date.getTime()) / MS_PER_HOUR;
+ console.log(expiredTimeInHours >= targetDelayInHours ? 'yes' : 'no');
+}
+
+main();
diff --git a/uAssets/tools/update-3rdparties.sh b/uAssets/tools/update-3rdparties.sh
new file mode 100755
index 0000000..f007c0c
--- /dev/null
+++ b/uAssets/tools/update-3rdparties.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+#
+# This script assumes a linux environment
+
+TEMPFILE=$(mktemp)
+
+echo "*** uAssets: updating remote assets..."
+
+declare -A assets
+assets=(
+ ['thirdparties/pgl.yoyo.org/as/serverlist']='https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&startdate%5Bday%5D=&startdate%5Bmonth%5D=&startdate%5Byear%5D=&mimetype=plaintext'
+ ['thirdparties/publicsuffix.org/list/effective_tld_names.dat']='https://publicsuffix.org/list/public_suffix_list.dat'
+ ['thirdparties/urlhaus-filter/urlhaus-filter-online.txt']='https://malware-filter.gitlab.io/urlhaus-filter/urlhaus-filter-online.txt'
+)
+
+for i in "${!assets[@]}"; do
+ localURL="$i"
+ remoteURL="${assets[$i]}"
+ echo "*** Downloading ${remoteURL}"
+ if wget -q -T 30 -O "$TEMPFILE" -- "$remoteURL"; then
+ if [ -s "$TEMPFILE" ]; then
+ if ! cmp -s "$TEMPFILE" "$localURL"; then
+ echo " New version found: ${localURL}"
+ if [ "$1" != "dry" ]; then
+ mv "$TEMPFILE" "$localURL"
+ fi
+ fi
+ fi
+ fi
+done
diff --git a/uAssets/tools/update-diffpatches.sh b/uAssets/tools/update-diffpatches.sh
new file mode 100755
index 0000000..406598e
--- /dev/null
+++ b/uAssets/tools/update-diffpatches.sh
@@ -0,0 +1,115 @@
+#!/usr/bin/env bash
+#
+# This script assumes a linux environment
+
+set -e
+
+# To be executed at the root of CDN repo
+#
+# It's not being hosted at CDN because that
+# repo is also used as a website
+
+REPO_DIR=$1
+if [[ -z $REPO_DIR ]]; then
+ echo "Error: repo directory is not provided, aborting"
+ exit 1
+fi
+
+PATCHES_DIR=$2
+if [[ -z $PATCHES_DIR ]]; then
+ echo "Error: patches directory is not provided, aborting"
+ exit 1
+fi
+
+FILTER_FILES=$3
+if [[ -z $FILTER_FILES ]]; then
+ echo "Error: filter lists are not provided, aborting"
+ exit 1
+fi
+FILTER_FILES=( "$FILTER_FILES" )
+
+PATCH_FILES=( $(ls -1v "$PATCHES_DIR"/*.patch | head -n -1) )
+
+# Keep only the most recent (5-day x 4-per-day) patches
+OBSOLETE_PATCHES=( $(ls -1v "$PATCHES_DIR"/*.patch | head -n -20) )
+for FILE in "${OBSOLETE_PATCHES[@]}"; do
+ echo "Removing obsolete patch $FILE"
+ git rm "$FILE"
+done
+
+NEW_PATCH_FILE=$(mktemp)
+DIFF_FILE=$(mktemp)
+
+for PATCH_FILE in "${PATCH_FILES[@]}"; do
+
+ # Extract tag from patch file name
+ [[ ${PATCH_FILE} =~ ^$PATCHES_DIR/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\.patch$ ]] && \
+ PREVIOUS_VERSION=${BASH_REMATCH[1]}
+
+ # This will receive a clone of an old version of the current repo
+ echo "Fetching repo at $PREVIOUS_VERSION version"
+ OLD_REPO=$(mktemp -d)
+ git clone -q --single-branch --branch "$PREVIOUS_VERSION" --depth=1 "https://github.com/$REPO_DIR.git" "$OLD_REPO" 2>/dev/null || true
+
+ # Skip if version doesn't exist
+ if [ -z "$(ls -A "$OLD_REPO" 2>/dev/null)" ]; then
+ continue;
+ fi
+
+ : > "$NEW_PATCH_FILE"
+
+ for FILTER_LIST in ${FILTER_FILES[@]}; do
+
+ if [ ! -f "$OLD_REPO/$FILTER_LIST" ]; then continue; fi
+
+ # Patches are for filter lists supporting differential updates
+ if ! (head "$OLD_REPO/$FILTER_LIST" | grep -q '^! Diff-Path: '); then
+ continue
+ fi
+
+ # Reference:
+ # https://github.com/ameshkov/diffupdates
+
+ # Extract diff name from `! Diff-Path:` field
+ DIFF_NAME=$(grep -m 1 -oP '^! Diff-Path: [^#]+#?\K.*' "$FILTER_LIST")
+ # Fall back to `! Diff-Name:` field if no name found
+ # Remove once `! Diff-Name:` is no longer needed after transition
+ if [[ -z $DIFF_NAME ]]; then
+ DIFF_NAME=$(grep -m 1 -oP '^! Diff-Name: \K.+' "$FILTER_LIST")
+ fi
+
+ # We need a patch name to generate a valid patch
+ if [[ -z $DIFF_NAME ]]; then
+ echo "Info: $FILTER_LIST is missing a patch name, skipping"
+ continue
+ fi
+
+ # Compute the RCS diff between current version and new version
+ diff -n "$OLD_REPO/$FILTER_LIST" "$FILTER_LIST" > "$DIFF_FILE" || true
+
+ FILE_CHECKSUM=$(sha1sum "$FILTER_LIST")
+ FILE_CHECKSUM=${FILE_CHECKSUM:0:10}
+
+ DIFF_LINE_COUNT=$(wc -l < "$DIFF_FILE")
+
+ # Patch header
+ DIFF_HEAD="diff name:$DIFF_NAME lines:$DIFF_LINE_COUNT checksum:$FILE_CHECKSUM"
+ printf "\tAdding diff: %s\n" "$DIFF_HEAD"
+ echo "$DIFF_HEAD" >> "$NEW_PATCH_FILE"
+ # Patch data
+ cat "$DIFF_FILE" >> "$NEW_PATCH_FILE"
+
+ done
+
+ rm -rf "$OLD_REPO"
+
+ # Stage changed patch file
+ mv -f "$NEW_PATCH_FILE" "$PATCH_FILE"
+ ls -l "$PATCH_FILE"
+ echo "Info: Staging ${PATCH_FILE}"
+ git add -u "$PATCH_FILE"
+
+done
+
+rm -f "$DIFF_FILE"
+rm -f "$NEW_PATCH_FILE"
diff --git a/uAssets/tools/validate/config.js b/uAssets/tools/validate/config.js
new file mode 100644
index 0000000..270aedb
--- /dev/null
+++ b/uAssets/tools/validate/config.js
@@ -0,0 +1,33 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2022-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+// jshint node:true, esversion:8, laxbreak:true
+
+'use strict';
+
+export default {
+ dnsQueries: [
+ 'https://cloudflare-dns.com/dns-query?name=${hn}&type=A',
+ 'https://dns.google/resolve?name=${hn}&type=A',
+ ],
+ // ms
+ throttle: 250,
+};
diff --git a/uAssets/tools/validate/package.json b/uAssets/tools/validate/package.json
new file mode 100644
index 0000000..c10527a
--- /dev/null
+++ b/uAssets/tools/validate/package.json
@@ -0,0 +1,6 @@
+{
+ "engines": {
+ "node": ">=17.5.0"
+ },
+ "type": "module"
+}
diff --git a/uAssets/tools/validate/validate.js b/uAssets/tools/validate/validate.js
new file mode 100644
index 0000000..ce448f8
--- /dev/null
+++ b/uAssets/tools/validate/validate.js
@@ -0,0 +1,321 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2022-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+// jshint node:true, esversion:8, laxbreak:true
+
+'use strict';
+
+/******************************************************************************/
+
+import fs from 'fs/promises';
+import https from 'https';
+import path from 'path';
+import process from 'process';
+
+import { StaticFilteringParser } from './uBlock/src/js/static-filtering-parser.js';
+import { LineIterator } from './uBlock/src/js/text-utils.js';
+
+import config from './config.js';
+
+/******************************************************************************/
+
+const commandLineArgs = (( ) => {
+ const args = new Map();
+ let name, value;
+ for ( const arg of process.argv.slice(2) ) {
+ const pos = arg.indexOf('=');
+ if ( pos === -1 ) {
+ name = arg;
+ value = '';
+ } else {
+ name = arg.slice(0, pos);
+ value = arg.slice(pos+1).trim();
+ }
+ args.set(name, value);
+ }
+ return args;
+})();
+
+/******************************************************************************/
+
+const stdOutput = [];
+
+const log = (text, silent = false) => {
+ stdOutput.push(text);
+ if ( silent === false ) {
+ console.log(text);
+ }
+};
+
+/******************************************************************************/
+
+const jsonSetMapReplacer = (k, v) => {
+ if ( v instanceof Set || v instanceof Map ) {
+ if ( v.size === 0 ) { return; }
+ return Array.from(v);
+ }
+ return v;
+};
+
+/******************************************************************************/
+
+const writeFile = async (fname, data) => {
+ const dir = path.dirname(fname);
+ await fs.mkdir(dir, { recursive: true });
+ const promise = fs.writeFile(fname, data);
+ writeOps.push(promise);
+ return promise;
+};
+const writeOps = [];
+
+/******************************************************************************/
+
+function sleep(ms) {
+ return new Promise(resolve => {
+ setTimeout(( ) => { resolve(); }, ms);
+ });
+}
+
+/******************************************************************************/
+
+// https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/
+
+async function validateHostnameWithQuery(url) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ headers: {
+ accept: 'application/dns-json',
+ }
+ };
+ https.get(url, options, response => {
+ const data = [];
+ response.on('data', chunk => {
+ data.push(chunk.toString());
+ });
+ response.on('end', ( ) => {
+ let result;
+ try {
+ result = JSON.parse(data.join(''));
+ } catch(ex) {
+ }
+ resolve(result);
+ });
+ }).on('error', error => {
+ resolve();
+ });
+ });
+}
+
+async function validateHostname(hn) {
+ await sleep(config.throttle);
+ for ( const dnsQuery of config.dnsQueries ) {
+ const url = dnsQuery.replace('${hn}', hn);
+ const result = await validateHostnameWithQuery(url);
+ if ( result !== undefined && result.Status !== 2 ) { return result; }
+ }
+}
+
+/******************************************************************************/
+
+function parseHostnameList(parser, s, hostnames) {
+ let beg = 0;
+ let slen = s.length;
+ while ( beg < slen ) {
+ let end = s.indexOf('|', beg);
+ if ( end === -1 ) { end = slen; }
+ const hn = parser.normalizeHostnameValue(s.slice(beg, end));
+ beg = end + 1;
+ if ( hn === undefined ) { continue; }
+ if ( hn.includes('*') ) { continue; }
+ hostnames.push(hn);
+ }
+ return hostnames;
+}
+
+/******************************************************************************/
+
+function processNet(parser) {
+ const hostnames = [];
+ if ( parser.patternIsPlainHostname() ) {
+ hostnames.push(parser.getPattern());
+ } else if ( parser.patternIsLeftHostnameAnchored() ) {
+ const match = /^([^/?]+)/.exec(parser.getPattern());
+ if (
+ match !== null &&
+ match[1].includes('*') === false &&
+ match[1].startsWith('.') === false &&
+ match[1].endsWith('.') === false
+ ) {
+ hostnames.push(match[0]);
+ }
+ }
+ if ( parser.hasOptions() === false ) { return hostnames; }
+ for ( const { id, val } of parser.netOptions() ) {
+ if ( id !== parser.OPTTokenDomain ) { continue; }
+ parseHostnameList(parser, val, hostnames);
+ }
+ return hostnames;
+}
+
+/******************************************************************************/
+
+function processExt(parser) {
+ const hostnames = [];
+ if ( parser.hasOptions() === false ) { return hostnames; }
+ for ( const { hn } of parser.extOptions() ) {
+ if ( hn.includes('*') ) { continue; }
+ hostnames.push(hn);
+ }
+ return hostnames;
+}
+
+/******************************************************************************/
+
+// https://www.rfc-editor.org/rfc/rfc1035.html
+
+function checkHostname(hn, result) {
+ if ( result instanceof Object === false ) { return; }
+ if ( result.Status === 1 ) { return `${hn} format error`; }
+ if ( result.Status === 2 ) { return `${hn} dns server failure`; }
+ if ( result.Status === 3 ) { return `${hn} name error`; }
+ if ( result.Status === 4 ) { return `${hn} not implemented`; }
+ if ( result.Status === 5 ) { return `${hn} refused`; }
+ if ( result.Answer === undefined ) { return; }
+ for ( const entry of result.Answer ) {
+ if ( entry.data === undefined ) { continue; }
+ for ( const re of parkedDomainAuthorities ) {
+ if ( re.test(entry.data) === false ) { continue; }
+ return `${hn} parked`;
+ }
+ }
+}
+
+const parkedDomainAuthorities = [
+ /^traff-\d+\.hugedomains\.com\.?$/,
+ /^\d+\.parkingcrew\.net\.?$/,
+ /^ns\d\.centralnic\.net\.?(\s|$)/,
+ /^ns\d\.pananames\.com\.?(\s|$)/,
+];
+
+/******************************************************************************/
+
+function toProgressString(lineno, hn) {
+ const parts = [];
+ if ( lineno > 0 ) { parts.push(`${lineno}`); }
+ if ( hn ) { parts.push(hn); }
+ const s = parts.join(' ');
+ process.stdout.write(`\r${s.padEnd(lastProgressStr.length)}\r`);
+ lastProgressStr = s;
+}
+
+let lastProgressStr = '';
+
+/******************************************************************************/
+
+// TODO: resume from partial results
+
+async function processList(parser, text, lineto, fpath) {
+
+ const lineIter = new LineIterator(text);
+ const lines = [];
+
+ while ( lineIter.eot() === false ) {
+ lines.push(lineIter.next());
+ }
+
+ if ( lineto === undefined ) {
+ lineto = lines.length;
+ }
+
+ for ( let i = lines.length; i > 0; i-- ) {
+ if ( i > lineto ) { continue; }
+ toProgressString(i);
+
+ let line = lines[i-1];
+
+ parser.analyze(line);
+
+ if ( parser.shouldIgnore() ) { continue; }
+
+ let hostnames;
+ if ( parser.category !== parser.CATStaticNetFilter ) {
+ hostnames = processExt(parser);
+ } else if ( parser.patternHasUnicode() === false || parser.toASCII() ) {
+ hostnames = processNet(parser);
+ }
+ const badHostnames = [];
+ for ( const hn of hostnames ) {
+ if ( hn.endsWith('.onion') ) { continue; }
+ if ( /^\d+\.\d+\.\d+\.\d+$/.test(hn) ) { continue; }
+ let result = validatedHostnames.get(hn);
+ if ( result === undefined ) {
+ toProgressString(i, hn);
+ result = await validateHostname(hn);
+ validatedHostnames.set(hn, result);
+ }
+ const diagnostic = checkHostname(hn, result);
+ if ( diagnostic === undefined ) { continue; }
+ badHostnames.push(diagnostic);
+ }
+ if ( badHostnames.length !== 0 ) {
+ toProgressString(0);
+ const lineno = i;
+ badHostnames.forEach(v => {
+ log(`${lineno} ${v}`);
+ });
+ writeFile(fpath, stdOutput.join('\n'));
+ }
+ }
+ toProgressString(0);
+}
+
+const validatedHostnames = new Map();
+
+/******************************************************************************/
+
+async function main() {
+ const infile = commandLineArgs.get('in');
+ if ( infile === undefined || infile === '' ) { return; }
+ const outdir = commandLineArgs.get('out');
+ if ( outdir === undefined || outdir === '' ) { return; }
+
+ const infileParts = path.parse(infile);
+ const lineto = commandLineArgs.get('line') !== undefined
+ ? parseInt(commandLineArgs.get('line'), 10)
+ : undefined;
+
+ const partialResultPath = `${outdir}/${infileParts.name}.results.partial.txt`;
+ const parser = new StaticFilteringParser();
+
+ const text = await fs.readFile(infile, { encoding: 'utf8' });
+ await processList(parser, text, lineto, partialResultPath);
+
+ writeFile(`${outdir}/${infileParts.name}.results.txt`, stdOutput.join('\n'));
+ writeFile(`${outdir}/${infileParts.name}.dns.results.txt`, JSON.stringify(validatedHostnames, jsonSetMapReplacer, 1));
+
+ fs.rm(partialResultPath);
+
+ await Promise.all(writeOps);
+}
+
+main();
+
+/******************************************************************************/