Skip to content

Commit 43b36ae

Browse files
authored
⚡️ 压缩用户网络规则的 DNR 编译输出 (#1733)
1 parent 12fa384 commit 43b36ae

3 files changed

Lines changed: 411 additions & 16 deletions

File tree

‎src/app/service/service_worker/network_rule.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@ describe("NetworkRuleService", () => {
162162
expect(reordered.outcome).toBe("applied");
163163
expect(reordered.state.order).toEqual([ids[1], ids[0]]);
164164
const compiled = applier.apply.mock.calls.at(-1)![0];
165-
expect(compiled.map((item) => item.condition.requestDomains)).toEqual([["b.example.com"], ["a.example.com"]]);
166-
expect(compiled[0].priority).toBeGreaterThan(compiled[1].priority!);
165+
expect(compiled.map((item) => item.condition.requestDomains)).toEqual([["a.example.com", "b.example.com"]]);
166+
expect(compiled).toHaveLength(1);
167+
expect(compiled[0].priority).toBe(2);
167168
});
168169

169170
it("批量删除三条只写一次 state、只更新一次动态规则,并同步移出顺序数组", async () => {

‎src/app/service/service_worker/network_rule_compiler.test.ts‎

Lines changed: 251 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it, vi } from "vitest";
2-
import type { NetworkRule, NetworkRuleState } from "@App/app/repo/network_rule";
2+
import type { NetworkRule, NetworkRuleCondition, NetworkRuleState } from "@App/app/repo/network_rule";
33
import { compileNetworkRules, DeclarativeNetRequestUserRuleApplier } from "./network_rule_compiler";
44
import {
55
INSTALL_GUARD_RULE_ID_MAX,
@@ -22,12 +22,17 @@ type DnrMock = typeof chrome.declarativeNetRequest & {
2222

2323
const dnr = chrome.declarativeNetRequest as DnrMock;
2424

25-
function rule(id: string, action: NetworkRule["action"], enabled = true): NetworkRule {
25+
function rule(
26+
id: string,
27+
action: NetworkRule["action"],
28+
enabled = true,
29+
condition: NetworkRuleCondition = { requestDomains: [`${id}.example.com`] }
30+
): NetworkRule {
2631
return {
2732
id,
2833
name: id,
2934
enabled,
30-
condition: { requestDomains: [`${id}.example.com`] },
35+
condition,
3136
action,
3237
createdAt: 1,
3338
updatedAt: 1,
@@ -81,6 +86,249 @@ function requestPhase(rules: chrome.declarativeNetRequest.Rule[]): chrome.declar
8186
}
8287

8388
describe("网络规则 DNR 编译与对账", () => {
89+
it("相邻的纯响应头移除规则合并 requestDomains,并保留最高优先级", () => {
90+
const compiled = compileNetworkRules(
91+
state([
92+
rule("a", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
93+
rule("b", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
94+
])
95+
);
96+
97+
expect(compiled).toHaveLength(1);
98+
expect(compiled[0]).toMatchObject({
99+
id: USER_RULE_ID_MIN,
100+
priority: 2,
101+
action: {
102+
type: "modifyHeaders",
103+
responseHeaders: [{ header: "content-security-policy", operation: "remove" }],
104+
},
105+
condition: {
106+
requestDomains: ["a.example.com", "b.example.com"],
107+
},
108+
});
109+
});
110+
111+
it("100 条单域名规则合并为一条,超过 100 个域名时按物理上限分块", () => {
112+
const rules = Array.from({ length: 101 }, (_, index) =>
113+
rule(`site-${String(index).padStart(3, "0")}`, {
114+
type: "removeResponseHeaders",
115+
headers: ["content-security-policy"],
116+
})
117+
);
118+
119+
const compiled = compileNetworkRules(state(rules));
120+
121+
expect(compiled).toHaveLength(2);
122+
expect(compiled[0].condition.requestDomains).toHaveLength(100);
123+
expect(compiled[1].condition.requestDomains).toEqual(["site-100.example.com"]);
124+
expect(compiled.flatMap((item) => item.condition.requestDomains)).toEqual(
125+
rules.map((item) => item.condition.requestDomains![0]).sort()
126+
);
127+
expect(compiled.map((item) => item.priority)).toEqual([101, 101]);
128+
expect(compiled.map((item) => item.id)).toEqual([USER_RULE_ID_MIN, USER_RULE_ID_MIN + 1]);
129+
});
130+
131+
it("合并前去重并消除已被父域覆盖的 requestDomains", () => {
132+
const compiled = compileNetworkRules(
133+
state([
134+
rule("parent", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
135+
requestDomains: ["example.com", "foo.example.com", "badexample.com"],
136+
}),
137+
rule("child", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
138+
requestDomains: ["example.com", "bar.foo.example.com", "other.com"],
139+
}),
140+
])
141+
);
142+
143+
expect(compiled).toHaveLength(1);
144+
expect(compiled[0].condition.requestDomains).toEqual(["badexample.com", "example.com", "other.com"]);
145+
});
146+
147+
it("不把 IPv4-like 或 IPv6-like host 当作可做父域消除的 DNS domain", () => {
148+
const compiled = compileNetworkRules(
149+
state([
150+
rule("ipv4-parent", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
151+
requestDomains: ["2.3.4"],
152+
}),
153+
rule("ipv4-child", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
154+
requestDomains: ["1.2.3.4"],
155+
}),
156+
rule("ipv6-parent", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
157+
requestDomains: ["[2001:db8::1]"],
158+
}),
159+
rule("ipv6-child", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
160+
requestDomains: ["[2001:db8::2]"],
161+
}),
162+
])
163+
);
164+
165+
expect(compiled.map((item) => item.condition.requestDomains)).toEqual([
166+
["1.2.3.4", "2.3.0.4", "[2001:db8::1]", "[2001:db8::2]"],
167+
]);
168+
});
169+
170+
it("compiled condition 的 set-like array 顺序不同仍可合并", () => {
171+
const compiled = compileNetworkRules(
172+
state([
173+
rule("a", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
174+
requestDomains: ["a.example.com"],
175+
resourceTypes: ["script", "main_frame"],
176+
requestMethods: ["post", "get"],
177+
}),
178+
rule("b", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
179+
requestDomains: ["b.example.com"],
180+
resourceTypes: ["main_frame", "script"],
181+
requestMethods: ["get", "post"],
182+
}),
183+
])
184+
);
185+
186+
expect(compiled).toHaveLength(1);
187+
expect(compiled[0].condition.requestDomains).toEqual(["a.example.com", "b.example.com"]);
188+
});
189+
190+
it.each([
191+
[
192+
"resourceTypes",
193+
{ requestDomains: ["a.example.com"], resourceTypes: ["main_frame"] },
194+
{ requestDomains: ["b.example.com"], resourceTypes: ["script"] },
195+
],
196+
[
197+
"requestMethods",
198+
{ requestDomains: ["a.example.com"], requestMethods: ["get"] },
199+
{ requestDomains: ["b.example.com"], requestMethods: ["post"] },
200+
],
201+
[
202+
"excludedRequestDomains",
203+
{ requestDomains: ["a.example.com"], excludedRequestDomains: ["cdn.example.com"] },
204+
{ requestDomains: ["b.example.com"], excludedRequestDomains: ["static.example.com"] },
205+
],
206+
[
207+
"urlFilter",
208+
{ requestDomains: ["a.example.com"], urlFilter: "*/a/*" },
209+
{ requestDomains: ["b.example.com"], urlFilter: "*/b/*" },
210+
],
211+
] as Array<[string, NetworkRuleCondition, NetworkRuleCondition]>)(
212+
"除 requestDomains 外的 %s 差异是 barrier",
213+
(_field, firstCondition, secondCondition) => {
214+
const compiled = compileNetworkRules(
215+
state([
216+
rule(
217+
"barrier-a",
218+
{ type: "removeResponseHeaders", headers: ["content-security-policy"] },
219+
true,
220+
firstCondition
221+
),
222+
rule(
223+
"barrier-b",
224+
{ type: "removeResponseHeaders", headers: ["content-security-policy"] },
225+
true,
226+
secondCondition
227+
),
228+
])
229+
);
230+
231+
expect(compiled).toHaveLength(2);
232+
}
233+
);
234+
235+
it("不同的响应头移除 action 不合并", () => {
236+
const compiled = compileNetworkRules(
237+
state([
238+
rule("csp", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
239+
rule("csp-frame", {
240+
type: "removeResponseHeaders",
241+
headers: ["content-security-policy", "x-frame-options"],
242+
}),
243+
])
244+
);
245+
246+
expect(compiled).toHaveLength(2);
247+
});
248+
249+
it("相同的响应头移除集合即使排列不同也可合并,并输出 canonical action", () => {
250+
const compiled = compileNetworkRules(
251+
state([
252+
rule("a", { type: "removeResponseHeaders", headers: ["x-test", "content-security-policy"] }),
253+
rule("b", { type: "removeResponseHeaders", headers: ["content-security-policy", "x-test"] }),
254+
])
255+
);
256+
257+
expect(compiled).toHaveLength(1);
258+
expect(compiled[0].action.responseHeaders).toEqual([
259+
{ header: "content-security-policy", operation: "remove" },
260+
{ header: "x-test", operation: "remove" },
261+
]);
262+
});
263+
264+
it("active rule barrier 不能被跨越,但 disabled rule 不阻断合并", () => {
265+
const baseRules = [
266+
rule("first", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
267+
rule("barrier", { type: "allow" }, false),
268+
rule("last", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
269+
];
270+
271+
expect(compileNetworkRules(state(baseRules))).toHaveLength(1);
272+
273+
const enabledRules = baseRules.map((item) => (item.id === "barrier" ? { ...item, enabled: true } : item));
274+
expect(compileNetworkRules(state(enabledRules))).toHaveLength(3);
275+
});
276+
277+
it("没有 requestDomains 的规则不参与合并", () => {
278+
const compiled = compileNetworkRules(
279+
state([
280+
rule("a", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
281+
urlFilter: "*/a/*",
282+
}),
283+
rule("b", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
284+
urlFilter: "*/b/*",
285+
}),
286+
])
287+
);
288+
289+
expect(compiled).toHaveLength(2);
290+
});
291+
292+
it("空 requestDomains 也不参与合并", () => {
293+
const compiled = compileNetworkRules(
294+
state([
295+
rule("a", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
296+
requestDomains: [],
297+
}),
298+
rule("b", { type: "removeResponseHeaders", headers: ["content-security-policy"] }, true, {
299+
requestDomains: [],
300+
}),
301+
])
302+
);
303+
304+
expect(compiled).toHaveLength(2);
305+
});
306+
307+
it("非纯响应头移除 action 不参与合并", () => {
308+
const compiled = compileNetworkRules(
309+
state([
310+
rule("a", { type: "modifyResponseHeaders", headers: [{ header: "x-test", operation: "remove" }] }),
311+
rule("b", { type: "modifyResponseHeaders", headers: [{ header: "x-test", operation: "remove" }] }),
312+
])
313+
);
314+
315+
expect(compiled).toHaveLength(2);
316+
});
317+
318+
it("编译 deterministic 且不 mutate source state", () => {
319+
const input = state([
320+
rule("a", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
321+
rule("b", { type: "removeResponseHeaders", headers: ["content-security-policy"] }),
322+
]);
323+
const original = structuredClone(input);
324+
325+
const first = compileNetworkRules(input);
326+
const second = compileNetworkRules(input);
327+
328+
expect(first).toEqual(second);
329+
expect(input).toEqual(original);
330+
});
331+
84332
it("一次应用把两条不同动作的规则编译成保留段内的两条 DNR 规则,回收段内陈旧 ID 并保留段外规则", async () => {
85333
dnr.resetMock();
86334
await chrome.declarativeNetRequest.updateDynamicRules({

0 commit comments

Comments
 (0)