Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .github/workflows/validate-dnr-rules.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Build validate-dnr-rules

on:
push:
branches: [ghostery]
pull_request:
branches: [ghostery]
workflow_dispatch:

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
name: linux-x64
deps: cmake ninja-build pkg-config ruby unifdef libicu-dev g++ perl python3
- os: macos-15
name: macos-arm64
deps: cmake ninja icu4c pkg-config
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends ${{ matrix.deps }}

- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew install ${{ matrix.deps }}

- name: Cache CMake build
uses: actions/cache@v4
with:
path: build
key: cmake-${{ matrix.name }}-${{ hashFiles('Source/WTF/**', 'Source/WebCore/contentextensions/**', 'ghostery/validate-dnr-rules/**') }}
restore-keys: |
cmake-${{ matrix.name }}-

- name: Configure
run: |
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DPORT=JSCOnly \
-DUSE_SYSTEM_MALLOC=ON \
.
env:
CMAKE_PREFIX_PATH: ${{ runner.os == 'macOS' && '/opt/homebrew/opt/icu4c' || '' }}

- name: Build
run: cmake --build build --target validate-dnr-rules

- name: Test
run: |
cat > /tmp/valid-rules.json << 'RULES'
[
{"id":1,"priority":1,"action":{"type":"block"},"condition":{"regexFilter":"ads\\.example\\.com"}},
{"id":2,"priority":1,"action":{"type":"block"},"condition":{"urlFilter":"||tracker.example.com^"}}
]
RULES
./build/bin/validate-dnr-rules /tmp/valid-rules.json

cat > /tmp/invalid-rules.json << 'RULES'
[
{"id":1,"priority":1,"action":{"type":"block"},"condition":{"regexFilter":"ad[0-9]{2}\\.js"}},
{"id":2,"priority":1,"action":{"type":"block"},"condition":{"regexFilter":"(?:ads|tracking)\\.com"}}
]
RULES
if ./build/bin/validate-dnr-rules /tmp/invalid-rules.json; then
echo "Expected validator to exit non-zero for invalid rules"
exit 1
fi

- name: Sign binary (macOS)
if: runner.os == 'macOS'
run: codesign --sign - --force build/bin/validate-dnr-rules

- name: Prepare artifact
run: |
cp build/bin/validate-dnr-rules validate-dnr-rules-${{ matrix.name }}
chmod +x validate-dnr-rules-${{ matrix.name }}

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: validate-dnr-rules-${{ matrix.name }}
path: validate-dnr-rules-${{ matrix.name }}

release:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/ghostery'
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Create release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
TAG="validate-dnr-rules-$(date +%Y%m%d)-${GITHUB_SHA::8}"

# Delete existing release with same tag if re-running
gh release delete "$TAG" --yes 2>/dev/null || true

gh release create "$TAG" \
--title "validate-dnr-rules $(date +%Y-%m-%d)" \
--notes "Automated build from commit ${GITHUB_SHA::8}.

## Downloads
- **Linux x64**: \`validate-dnr-rules-linux-x64\`
- **macOS arm64**: \`validate-dnr-rules-macos-arm64\` (Intel Macs: run via Rosetta)

## Usage
\`\`\`
chmod +x validate-dnr-rules-*
./validate-dnr-rules-linux-x64 path/to/dnr-rules.json
\`\`\`" \
artifacts/validate-dnr-rules-linux-x64/validate-dnr-rules-linux-x64 \
artifacts/validate-dnr-rules-macos-arm64/validate-dnr-rules-macos-arm64
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ if (DEVELOPER_MODE)
add_subdirectory(PerformanceTests)
endif ()

# -----------------------------------------------------------------------------
# Ghostery tools
# -----------------------------------------------------------------------------
if (EXISTS "${CMAKE_SOURCE_DIR}/ghostery/validate-dnr-rules/CMakeLists.txt")
add_subdirectory(ghostery/validate-dnr-rules)
endif ()

# -----------------------------------------------------------------------------
# Print the features list last, for maximum visibility.
# -----------------------------------------------------------------------------
Expand Down
195 changes: 195 additions & 0 deletions Tools/Scripts/validate-dnr-rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/bin/bash
# validate-dnr-rules - Validate Declarative Net Request rulesets using WebKit's translator
#
# Usage: validate-dnr-rules [--compile] <rules.json> [<rules.json> ...]
#
# Runs DNR rule files through WebKit's translation pipeline and reports errors.
# With --compile, also compiles translated rules to content blocker bytecode.
#
# Requires: WebKit built with Tools/Scripts/build-webkit --debug

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

COMPILE=0
FILES=()

for arg in "$@"; do
case "$arg" in
--compile) COMPILE=1 ;;
--help|-h)
echo "Usage: validate-dnr-rules [--compile] <rules.json> [<rules.json> ...]"
echo ""
echo "Validates DNR rulesets using WebKit's native translator pipeline."
echo "Reports translation errors (unsupported regex, invalid rules, etc)."
echo ""
echo "Options:"
echo " --compile Also compile translated rules to content blocker bytecode"
echo " --help Show this help"
exit 0
;;
*) FILES+=("$arg") ;;
esac
done

if [ ${#FILES[@]} -eq 0 ]; then
echo "Error: No input files specified. Use --help for usage." >&2
exit 1
fi

BUILD_DIR="$("$SCRIPT_DIR/webkit-build-directory" --configuration=Debug --top-level 2>/dev/null || true)"
if [ -z "$BUILD_DIR" ]; then
BUILD_DIR="$SOURCE_ROOT/WebKitBuild"
fi

FRAMEWORK_DIR="$BUILD_DIR/Debug"

if [ ! -d "$FRAMEWORK_DIR/WebKit.framework" ]; then
echo "Error: WebKit.framework not found at $FRAMEWORK_DIR" >&2
echo "Build WebKit first: Tools/Scripts/build-webkit --debug" >&2
exit 1
fi

TOOL_SRC=$(mktemp /tmp/validate-dnr-XXXXXX.mm)
TOOL_BIN=$(mktemp /tmp/validate-dnr-XXXXXX)

trap "rm -f '$TOOL_SRC' '$TOOL_BIN'" EXIT

cat > "$TOOL_SRC" << 'OBJC_SOURCE'
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
#import <WebKit/_WKWebExtensionDeclarativeNetRequestTranslator.h>
#import <WebKit/_WKWebExtensionDeclarativeNetRequestRule.h>
#import <WebKit/WKContentRuleListPrivate.h>

int main(int argc, const char *argv[]) {
@autoreleasepool {
BOOL doCompile = NO;
NSMutableArray<NSString *> *files = [NSMutableArray array];

for (int i = 1; i < argc; i++) {
NSString *arg = [NSString stringWithUTF8String:argv[i]];
if ([arg isEqualToString:@"--compile"])
doCompile = YES;
else
[files addObject:arg];
}

int totalErrors = 0;

for (NSString *filePath in files) {
NSData *data = [NSData dataWithContentsOfFile:filePath];
if (!data) {
fprintf(stderr, "ERROR: Cannot read file: %s\n", filePath.UTF8String);
totalErrors++;
continue;
}

NSString *rulesetID = [[filePath lastPathComponent] stringByDeletingPathExtension];
NSDictionary<NSString *, NSData *> *jsonDataDict = @{ rulesetID: data };

printf("=== %s ===\n", filePath.UTF8String);
printf("File size: %lu bytes\n", (unsigned long)data.length);

NSArray<NSString *> *jsonErrors = nil;
NSDictionary *allJSONObjects = [_WKWebExtensionDeclarativeNetRequestTranslator jsonObjectsFromData:jsonDataDict errorStrings:&jsonErrors];

if (jsonErrors.count > 0) {
printf("JSON deserialization errors: %lu\n", (unsigned long)jsonErrors.count);
for (NSString *error in jsonErrors) {
printf(" ERROR: %s\n", error.UTF8String);
totalErrors++;
}
}

NSUInteger ruleCount = 0;
for (NSString *key in allJSONObjects)
ruleCount += [allJSONObjects[key] count];
printf("Rules parsed: %lu\n", (unsigned long)ruleCount);

NSArray<NSString *> *translationErrors = nil;
NSArray *convertedRules = [_WKWebExtensionDeclarativeNetRequestTranslator translateRules:allJSONObjects errorStrings:&translationErrors];

printf("Rules translated: %lu\n", (unsigned long)convertedRules.count);

if (translationErrors.count > 0) {
printf("Translation errors: %lu\n", (unsigned long)translationErrors.count);
for (NSString *error in translationErrors) {
printf(" ERROR: %s\n", error.UTF8String);
totalErrors++;
}
}

if (doCompile && convertedRules.count > 0) {
NSError *jsonSerializationError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:convertedRules options:0 error:&jsonSerializationError];
if (jsonSerializationError) {
printf(" ERROR: JSON serialization failed: %s\n", jsonSerializationError.localizedDescription.UTF8String);
totalErrors++;
} else {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("Content blocker JSON: %lu bytes\n", (unsigned long)jsonData.length);
printf("Compiling...");
fflush(stdout);

__block BOOL done = NO;
__block BOOL success = NO;
__block NSString *compilationError = nil;

NSDate *startTime = [NSDate date];

[[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:rulesetID encodedContentRuleList:jsonString completionHandler:^(WKContentRuleList *ruleList, NSError *error) {
success = (ruleList != nil);
if (error)
compilationError = error.localizedDescription;
done = YES;
}];

while (!done)
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];

double elapsed = -[startTime timeIntervalSinceNow];

if (success)
printf(" OK (%.1f seconds)\n", elapsed);
else {
printf(" FAILED (%.1f seconds): %s\n", elapsed, compilationError.UTF8String);
totalErrors++;
}

[[WKContentRuleListStore defaultStore] removeContentRuleListForIdentifier:rulesetID completionHandler:^(NSError *error) {}];
}
}

printf("\n");
}

if (totalErrors)
printf("FAILED: %d error(s) found.\n", totalErrors);
else
printf("OK: All rules validated successfully.\n");

return totalErrors ? 1 : 0;
}
}
OBJC_SOURCE

clang -ObjC++ -std=c++20 -fobjc-arc -w \
-F"$FRAMEWORK_DIR" \
-framework WebKit -framework Foundation \
-lc++ \
-Wl,-rpath,"$FRAMEWORK_DIR" \
-o "$TOOL_BIN" "$TOOL_SRC"

ARGS=()
if [ "$COMPILE" -eq 1 ]; then
ARGS+=(--compile)
fi

for f in "${FILES[@]}"; do
ARGS+=("$(cd "$(dirname "$f")" && pwd)/$(basename "$f")")
done

DYLD_FRAMEWORK_PATH="$FRAMEWORK_DIR" "$TOOL_BIN" "${ARGS[@]}"
1 change: 1 addition & 0 deletions ghostery/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
validate-dnr-rules/build/
Loading
Loading