Skip to content

Commit 0a0d4ba

Browse files
authored
Add validate-dnr-rules CLI tool with cross-platform CI (#4)
Standalone tool for validating Declarative Net Request rulesets against WebKit's content extension translator. Ships prebuilt Linux x64 and macOS arm64 binaries from a GitHub Actions release workflow so Ghostery engineers can vet rulesets without a full WebKit build.
1 parent 1255971 commit 0a0d4ba

8 files changed

Lines changed: 610 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
name: Build validate-dnr-rules
2+
3+
on:
4+
push:
5+
branches: [ghostery]
6+
pull_request:
7+
branches: [ghostery]
8+
workflow_dispatch:
9+
10+
jobs:
11+
build:
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
include:
16+
- os: ubuntu-24.04
17+
name: linux-x64
18+
deps: cmake ninja-build pkg-config ruby unifdef libicu-dev g++ perl python3
19+
- os: macos-15
20+
name: macos-arm64
21+
deps: cmake ninja icu4c pkg-config
22+
runs-on: ${{ matrix.os }}
23+
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 1
28+
29+
- name: Install dependencies (Linux)
30+
if: runner.os == 'Linux'
31+
run: |
32+
sudo apt-get update
33+
sudo apt-get install -y --no-install-recommends ${{ matrix.deps }}
34+
35+
- name: Install dependencies (macOS)
36+
if: runner.os == 'macOS'
37+
run: brew install ${{ matrix.deps }}
38+
39+
- name: Cache CMake build
40+
uses: actions/cache@v4
41+
with:
42+
path: build
43+
key: cmake-${{ matrix.name }}-${{ hashFiles('Source/WTF/**', 'Source/WebCore/contentextensions/**', 'ghostery/validate-dnr-rules/**') }}
44+
restore-keys: |
45+
cmake-${{ matrix.name }}-
46+
47+
- name: Configure
48+
run: |
49+
cmake -B build -G Ninja \
50+
-DCMAKE_BUILD_TYPE=Release \
51+
-DPORT=JSCOnly \
52+
-DUSE_SYSTEM_MALLOC=ON \
53+
.
54+
env:
55+
CMAKE_PREFIX_PATH: ${{ runner.os == 'macOS' && '/opt/homebrew/opt/icu4c' || '' }}
56+
57+
- name: Build
58+
run: cmake --build build --target validate-dnr-rules
59+
60+
- name: Test
61+
run: |
62+
cat > /tmp/valid-rules.json << 'RULES'
63+
[
64+
{"id":1,"priority":1,"action":{"type":"block"},"condition":{"regexFilter":"ads\\.example\\.com"}},
65+
{"id":2,"priority":1,"action":{"type":"block"},"condition":{"urlFilter":"||tracker.example.com^"}}
66+
]
67+
RULES
68+
./build/bin/validate-dnr-rules /tmp/valid-rules.json
69+
70+
cat > /tmp/invalid-rules.json << 'RULES'
71+
[
72+
{"id":1,"priority":1,"action":{"type":"block"},"condition":{"regexFilter":"ad[0-9]{2}\\.js"}},
73+
{"id":2,"priority":1,"action":{"type":"block"},"condition":{"regexFilter":"(?:ads|tracking)\\.com"}}
74+
]
75+
RULES
76+
if ./build/bin/validate-dnr-rules /tmp/invalid-rules.json; then
77+
echo "Expected validator to exit non-zero for invalid rules"
78+
exit 1
79+
fi
80+
81+
- name: Sign binary (macOS)
82+
if: runner.os == 'macOS'
83+
run: codesign --sign - --force build/bin/validate-dnr-rules
84+
85+
- name: Prepare artifact
86+
run: |
87+
cp build/bin/validate-dnr-rules validate-dnr-rules-${{ matrix.name }}
88+
chmod +x validate-dnr-rules-${{ matrix.name }}
89+
90+
- name: Upload artifact
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: validate-dnr-rules-${{ matrix.name }}
94+
path: validate-dnr-rules-${{ matrix.name }}
95+
96+
release:
97+
needs: build
98+
if: github.event_name == 'push' && github.ref == 'refs/heads/ghostery'
99+
runs-on: ubuntu-latest
100+
permissions:
101+
contents: write
102+
103+
steps:
104+
- name: Download all artifacts
105+
uses: actions/download-artifact@v4
106+
with:
107+
path: artifacts
108+
109+
- name: Create release
110+
env:
111+
GH_TOKEN: ${{ github.token }}
112+
GH_REPO: ${{ github.repository }}
113+
run: |
114+
TAG="validate-dnr-rules-$(date +%Y%m%d)-${GITHUB_SHA::8}"
115+
116+
# Delete existing release with same tag if re-running
117+
gh release delete "$TAG" --yes 2>/dev/null || true
118+
119+
gh release create "$TAG" \
120+
--title "validate-dnr-rules $(date +%Y-%m-%d)" \
121+
--notes "Automated build from commit ${GITHUB_SHA::8}.
122+
123+
## Downloads
124+
- **Linux x64**: \`validate-dnr-rules-linux-x64\`
125+
- **macOS arm64**: \`validate-dnr-rules-macos-arm64\` (Intel Macs: run via Rosetta)
126+
127+
## Usage
128+
\`\`\`
129+
chmod +x validate-dnr-rules-*
130+
./validate-dnr-rules-linux-x64 path/to/dnr-rules.json
131+
\`\`\`" \
132+
artifacts/validate-dnr-rules-linux-x64/validate-dnr-rules-linux-x64 \
133+
artifacts/validate-dnr-rules-macos-arm64/validate-dnr-rules-macos-arm64

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ if (DEVELOPER_MODE)
5353
add_subdirectory(PerformanceTests)
5454
endif ()
5555

56+
# -----------------------------------------------------------------------------
57+
# Ghostery tools
58+
# -----------------------------------------------------------------------------
59+
if (EXISTS "${CMAKE_SOURCE_DIR}/ghostery/validate-dnr-rules/CMakeLists.txt")
60+
add_subdirectory(ghostery/validate-dnr-rules)
61+
endif ()
62+
5663
# -----------------------------------------------------------------------------
5764
# Print the features list last, for maximum visibility.
5865
# -----------------------------------------------------------------------------

Tools/Scripts/validate-dnr-rules

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/bin/bash
2+
# validate-dnr-rules - Validate Declarative Net Request rulesets using WebKit's translator
3+
#
4+
# Usage: validate-dnr-rules [--compile] <rules.json> [<rules.json> ...]
5+
#
6+
# Runs DNR rule files through WebKit's translation pipeline and reports errors.
7+
# With --compile, also compiles translated rules to content blocker bytecode.
8+
#
9+
# Requires: WebKit built with Tools/Scripts/build-webkit --debug
10+
11+
set -euo pipefail
12+
13+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14+
SOURCE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
15+
16+
COMPILE=0
17+
FILES=()
18+
19+
for arg in "$@"; do
20+
case "$arg" in
21+
--compile) COMPILE=1 ;;
22+
--help|-h)
23+
echo "Usage: validate-dnr-rules [--compile] <rules.json> [<rules.json> ...]"
24+
echo ""
25+
echo "Validates DNR rulesets using WebKit's native translator pipeline."
26+
echo "Reports translation errors (unsupported regex, invalid rules, etc)."
27+
echo ""
28+
echo "Options:"
29+
echo " --compile Also compile translated rules to content blocker bytecode"
30+
echo " --help Show this help"
31+
exit 0
32+
;;
33+
*) FILES+=("$arg") ;;
34+
esac
35+
done
36+
37+
if [ ${#FILES[@]} -eq 0 ]; then
38+
echo "Error: No input files specified. Use --help for usage." >&2
39+
exit 1
40+
fi
41+
42+
BUILD_DIR="$("$SCRIPT_DIR/webkit-build-directory" --configuration=Debug --top-level 2>/dev/null || true)"
43+
if [ -z "$BUILD_DIR" ]; then
44+
BUILD_DIR="$SOURCE_ROOT/WebKitBuild"
45+
fi
46+
47+
FRAMEWORK_DIR="$BUILD_DIR/Debug"
48+
49+
if [ ! -d "$FRAMEWORK_DIR/WebKit.framework" ]; then
50+
echo "Error: WebKit.framework not found at $FRAMEWORK_DIR" >&2
51+
echo "Build WebKit first: Tools/Scripts/build-webkit --debug" >&2
52+
exit 1
53+
fi
54+
55+
TOOL_SRC=$(mktemp /tmp/validate-dnr-XXXXXX.mm)
56+
TOOL_BIN=$(mktemp /tmp/validate-dnr-XXXXXX)
57+
58+
trap "rm -f '$TOOL_SRC' '$TOOL_BIN'" EXIT
59+
60+
cat > "$TOOL_SRC" << 'OBJC_SOURCE'
61+
#import <Foundation/Foundation.h>
62+
#import <WebKit/WebKit.h>
63+
#import <WebKit/_WKWebExtensionDeclarativeNetRequestTranslator.h>
64+
#import <WebKit/_WKWebExtensionDeclarativeNetRequestRule.h>
65+
#import <WebKit/WKContentRuleListPrivate.h>
66+
67+
int main(int argc, const char *argv[]) {
68+
@autoreleasepool {
69+
BOOL doCompile = NO;
70+
NSMutableArray<NSString *> *files = [NSMutableArray array];
71+
72+
for (int i = 1; i < argc; i++) {
73+
NSString *arg = [NSString stringWithUTF8String:argv[i]];
74+
if ([arg isEqualToString:@"--compile"])
75+
doCompile = YES;
76+
else
77+
[files addObject:arg];
78+
}
79+
80+
int totalErrors = 0;
81+
82+
for (NSString *filePath in files) {
83+
NSData *data = [NSData dataWithContentsOfFile:filePath];
84+
if (!data) {
85+
fprintf(stderr, "ERROR: Cannot read file: %s\n", filePath.UTF8String);
86+
totalErrors++;
87+
continue;
88+
}
89+
90+
NSString *rulesetID = [[filePath lastPathComponent] stringByDeletingPathExtension];
91+
NSDictionary<NSString *, NSData *> *jsonDataDict = @{ rulesetID: data };
92+
93+
printf("=== %s ===\n", filePath.UTF8String);
94+
printf("File size: %lu bytes\n", (unsigned long)data.length);
95+
96+
NSArray<NSString *> *jsonErrors = nil;
97+
NSDictionary *allJSONObjects = [_WKWebExtensionDeclarativeNetRequestTranslator jsonObjectsFromData:jsonDataDict errorStrings:&jsonErrors];
98+
99+
if (jsonErrors.count > 0) {
100+
printf("JSON deserialization errors: %lu\n", (unsigned long)jsonErrors.count);
101+
for (NSString *error in jsonErrors) {
102+
printf(" ERROR: %s\n", error.UTF8String);
103+
totalErrors++;
104+
}
105+
}
106+
107+
NSUInteger ruleCount = 0;
108+
for (NSString *key in allJSONObjects)
109+
ruleCount += [allJSONObjects[key] count];
110+
printf("Rules parsed: %lu\n", (unsigned long)ruleCount);
111+
112+
NSArray<NSString *> *translationErrors = nil;
113+
NSArray *convertedRules = [_WKWebExtensionDeclarativeNetRequestTranslator translateRules:allJSONObjects errorStrings:&translationErrors];
114+
115+
printf("Rules translated: %lu\n", (unsigned long)convertedRules.count);
116+
117+
if (translationErrors.count > 0) {
118+
printf("Translation errors: %lu\n", (unsigned long)translationErrors.count);
119+
for (NSString *error in translationErrors) {
120+
printf(" ERROR: %s\n", error.UTF8String);
121+
totalErrors++;
122+
}
123+
}
124+
125+
if (doCompile && convertedRules.count > 0) {
126+
NSError *jsonSerializationError = nil;
127+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:convertedRules options:0 error:&jsonSerializationError];
128+
if (jsonSerializationError) {
129+
printf(" ERROR: JSON serialization failed: %s\n", jsonSerializationError.localizedDescription.UTF8String);
130+
totalErrors++;
131+
} else {
132+
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
133+
printf("Content blocker JSON: %lu bytes\n", (unsigned long)jsonData.length);
134+
printf("Compiling...");
135+
fflush(stdout);
136+
137+
__block BOOL done = NO;
138+
__block BOOL success = NO;
139+
__block NSString *compilationError = nil;
140+
141+
NSDate *startTime = [NSDate date];
142+
143+
[[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:rulesetID encodedContentRuleList:jsonString completionHandler:^(WKContentRuleList *ruleList, NSError *error) {
144+
success = (ruleList != nil);
145+
if (error)
146+
compilationError = error.localizedDescription;
147+
done = YES;
148+
}];
149+
150+
while (!done)
151+
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
152+
153+
double elapsed = -[startTime timeIntervalSinceNow];
154+
155+
if (success)
156+
printf(" OK (%.1f seconds)\n", elapsed);
157+
else {
158+
printf(" FAILED (%.1f seconds): %s\n", elapsed, compilationError.UTF8String);
159+
totalErrors++;
160+
}
161+
162+
[[WKContentRuleListStore defaultStore] removeContentRuleListForIdentifier:rulesetID completionHandler:^(NSError *error) {}];
163+
}
164+
}
165+
166+
printf("\n");
167+
}
168+
169+
if (totalErrors)
170+
printf("FAILED: %d error(s) found.\n", totalErrors);
171+
else
172+
printf("OK: All rules validated successfully.\n");
173+
174+
return totalErrors ? 1 : 0;
175+
}
176+
}
177+
OBJC_SOURCE
178+
179+
clang -ObjC++ -std=c++20 -fobjc-arc -w \
180+
-F"$FRAMEWORK_DIR" \
181+
-framework WebKit -framework Foundation \
182+
-lc++ \
183+
-Wl,-rpath,"$FRAMEWORK_DIR" \
184+
-o "$TOOL_BIN" "$TOOL_SRC"
185+
186+
ARGS=()
187+
if [ "$COMPILE" -eq 1 ]; then
188+
ARGS+=(--compile)
189+
fi
190+
191+
for f in "${FILES[@]}"; do
192+
ARGS+=("$(cd "$(dirname "$f")" && pwd)/$(basename "$f")")
193+
done
194+
195+
DYLD_FRAMEWORK_PATH="$FRAMEWORK_DIR" "$TOOL_BIN" "${ARGS[@]}"

ghostery/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
validate-dnr-rules/build/

0 commit comments

Comments
 (0)