match
Some checks failed
build / Build (push) Failing after 6s
test / Run tests (push) Failing after 8s

This commit is contained in:
2025-04-03 16:56:53 +08:00
parent fc69019877
commit eea3b43e0b
11 changed files with 460 additions and 66 deletions

View File

@ -1,10 +1,157 @@
import { describe, expect, it } from "vitest";
import { dealMatches } from "./match";
import { dealPatternMatches, parsePatternMatchesURL, UrlMatch } from "./match";
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
describe("dealMatches", () => {
it("*://link.17173.com*", () => {
const matches = dealMatches(["*://link.17173.com*"]);
expect(matches).toEqual(["*://link.17173.com/*"]);
describe("UrlMatch-google", () => {
const url = new UrlMatch<string>();
url.add("https://*/*", "ok1");
url.add("https://*/foo*", "ok2");
url.add("https://*.google.com/foo*bar", "ok3");
url.add("https://example.org/foo/bar.html", "ok4");
url.add("http://127.0.0.1/*", "ok5");
url.add("*://mail.google.com/*", "ok6");
it("match1", () => {
expect(url.match("https://www.google.com/")).toEqual(["ok1"]);
expect(url.match("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]);
});
it("match2", () => {
expect(url.match("https://example.com/foo/bar.html")).toEqual(["ok1", "ok2"]);
expect(url.match("https://www.google.com/foo")).toEqual(["ok1", "ok2"]);
expect(url.match("https://www.google.com/foo2")).toEqual(["ok1", "ok2"]);
});
it("match3", () => {
expect(url.match("https://www.google.com/foo/baz/bar")).toEqual(["ok1", "ok2", "ok3"]);
expect(url.match("https://docs.google.com/foobar")).toEqual(["ok1", "ok2", "ok3"]);
});
it("match4", () => {
expect(url.match("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]);
});
it("match5", () => {
expect(url.match("http://127.0.0.1/")).toEqual(["ok5"]);
expect(url.match("http://127.0.0.1/foo/bar.html")).toEqual(["ok5"]);
});
it("match6", () => {
expect(url.match("http://mail.google.com/foo/baz/bar")).toEqual(["ok6"]);
expect(url.match("https://mail.google.com/foobar")).toEqual(["ok1", "ok2", "ok3", "ok6"]);
});
});
describe("UrlMatch-google-error", () => {
const url = new UrlMatch<string>();
it("error-1", () => {
expect(() => {
url.add("https://*foo/bar", "ok1");
}).toThrow(Error);
});
it("error-2", () => {
expect(() => {
url.add("https://foo.*.bar/baz", "ok1");
}).toThrow(Error);
});
it("error-3", () => {
expect(() => {
url.add("http:/bar", "ok1");
}).toThrow(Error);
});
});
// 从tm找的一些特殊的匹配规则
describe("UrlMatch-search", () => {
const url = new UrlMatch<string>();
url.add("https://www.google.com/search?q=*", "ok1");
it("match1", () => {
expect(url.match("https://www.google.com/search?q=foo")).toEqual(["ok1"]);
expect(url.match("https://www.google.com/search?q1=foo")).toEqual([]);
});
url.add("https://bbs.tampermonkey.net.cn", "ok2");
it("match2", () => {
expect(url.match("https://bbs.tampermonkey.net.cn")).toEqual(["ok2"]);
expect(url.match("https://bbs.tampermonkey.net.cn/")).toEqual(["ok2"]);
expect(url.match("https://bbs.tampermonkey.net.cn/foo/bar.html")).toEqual([]);
});
});
describe("UrlMatch-port1", () => {
const url = new UrlMatch<string>();
url.add("http://test.list.ggnb.top/search", "ok1");
it("match1", () => {
expect(url.match("http://test.list.ggnb.top/search")).toEqual(["ok1"]);
expect(url.match("http://test.list.ggnb.top/search?")).toEqual([]);
expect(url.match("http://test.list.ggnb.top/search?foo=bar")).toEqual([]);
});
it("port", () => {
expect(url.match("http://test.list.ggnb.top:80/search")).toEqual(["ok1"]);
});
});
describe("UrlMatch-port2", () => {
const url = new UrlMatch<string>();
url.add("http://test.list.ggnb.top:80/search", "ok1");
url.add("http://test.list.ggnb.top*/search", "ok2");
url.add("http://test.list.ggnb.top:*/search", "ok3");
url.add("http://localhost:3000/", "ok4");
it("match1", () => {
expect(url.match("http://test.list.ggnb.top:80/search")).toEqual(["ok1", "ok2", "ok3"]);
expect(url.match("http://test.list.ggnb.top:81/search")).toEqual(["ok2", "ok3"]);
expect(url.match("http://test.list.ggnb.top/search")).toEqual(["ok1", "ok2", "ok3"]);
});
it("case2", () => {
expect(url.match("http://localhost:3000/")).toEqual(["ok4"]);
expect(url.match("http://localhost:8000/")).toEqual([]);
});
});
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
describe("dealPatternMatches", () => {
it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => {
const matches = dealPatternMatches(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]);
expect(matches.patternResult).toEqual(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]);
});
// 处理一些特殊情况
it("*://link.17173.com*", () => {
const matches = dealPatternMatches(["*://link.17173.com*"]);
expect(matches.patternResult).toEqual(["*://link.17173.com/*"]);
});
});
describe("parsePatternMatchesURL", () => {
it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => {
const matches = parsePatternMatchesURL("https://*/*");
expect(matches).toEqual({
scheme: "https",
host: "*",
path: "*",
});
const matches2 = parsePatternMatchesURL("https://*/foo*");
expect(matches2).toEqual({
scheme: "https",
host: "*",
path: "foo*",
});
const matches3 = parsePatternMatchesURL("http://127.0.0.1/");
expect(matches3).toEqual({
scheme: "http",
host: "127.0.0.1",
path: "",
});
});
it("search", () => {
// 会忽略掉search部分
const matches = parsePatternMatchesURL("https://*/*?search");
expect(matches).toEqual({
scheme: "https",
host: "*",
path: "*",
});
});
it("*://link.17173.com*", () => {
const matches = parsePatternMatchesURL("*://link.17173.com*");
expect(matches).toEqual({
scheme: "*",
host: "link.17173.com",
path: "*",
});
});
});

View File

@ -1,3 +1,5 @@
import Logger from "@App/app/logger/logger";
export interface Url {
scheme: string;
host: string;
@ -5,50 +7,269 @@ export interface Url {
search: string;
}
// 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行匹配
export class Match {}
export default class Match<T> {
protected cache = new Map<string, T[]>();
export function parseURL(url: string): Url | undefined {
const match = /^(.+?):\/\/(.*?)((\/.*?)(\?.*?|)|)$/.exec(url);
if (match) {
return {
scheme: match[1],
host: match[2],
path: match[4] || (url[url.length - 1] === "*" ? "*" : "/"),
search: match[5],
};
}
// 处理一些特殊情况
switch (url) {
case "*":
protected rule = new Map<string, T[]>();
protected parseURL(url: string): Url | undefined {
if (url.indexOf("*http") === 0) {
url = url.substring(1);
}
const match = /^(.+?):\/\/(.*?)((\/.*?)(\?.*?|)|)$/.exec(url);
if (match) {
return {
scheme: "*",
host: "*",
path: "*",
search: "*",
scheme: match[1],
host: match[2],
path: match[4] || (url[url.length - 1] === "*" ? "*" : "/"),
search: match[5],
};
default:
}
// 处理一些特殊情况
switch (url) {
case "*":
return {
scheme: "*",
host: "*",
path: "*",
search: "*",
};
default:
}
return undefined;
}
protected compileRe(url: string): string {
const u = this.parseURL(url);
if (!u) {
return "";
}
switch (u.scheme) {
case "*":
u.scheme = ".+?";
break;
case "http*":
u.scheme = "http[s]?";
break;
default:
}
let pos = u.host.indexOf("*");
if (u.host === "*" || u.host === "**") {
pos = -1;
} else if (u.host.endsWith("*")) {
// 处理*结尾
if (!u.host.endsWith(":*")) {
u.host = u.host.substring(0, u.host.length - 1);
}
} else if (pos !== -1 && pos !== 0) {
return "";
}
u.host = u.host.replace(/\*/g, "[^/]*?");
// 处理 *.开头
if (u.host.startsWith("[^/]*?.")) {
u.host = `([^/]*?\\.?)${u.host.substring(7)}`;
} else if (pos !== -1) {
if (u.host.indexOf(".") === -1) {
return "";
}
}
// 处理顶域
if (u.host.endsWith("tld")) {
u.host = `${u.host.substr(0, u.host.length - 3)}.*?`;
}
// 处理端口
const pos2 = u.host.indexOf(":");
if (pos2 === -1) {
u.host = `${u.host}(:\\d+)?`;
} else {
const port = u.host.substring(pos2 + 1);
if (port === "*") {
u.host = `${u.host.substring(0, pos2)}(:\\d+)?`;
} else {
u.host = `${u.host.substring(0, pos2)}(:${port})?`;
}
}
let re = `^${u.scheme}://${u.host}`;
if (u.path === "/") {
re += "[/]?";
} else {
re += u.path.replace(/\*/g, ".*?");
}
if (u.search) {
re += u.search.replace(/([\\?])/g, "\\$1").replace(/\*/g, ".*?");
}
return `${re.replace(/\//g, "/")}$`;
}
public add(url: string, val: T) {
const re = this.compileRe(url);
if (!re) {
throw new Error(`invalid url: ${url}`);
}
let rule = this.rule.get(re);
if (!rule) {
rule = [];
this.rule.set(re, rule);
}
rule.push(val);
this.delCache();
}
public match(url: string): T[] {
let ret = this.cache.get(url);
if (ret) {
return ret;
}
ret = [];
try {
this.rule.forEach((val, key) => {
const re = new RegExp(key);
if (re.test(url) && ret) {
ret.push(...val);
}
});
} catch (e) {
// eslint-disable-next-line no-console
console.warn("bad match rule", Logger.E(e));
// LoggerCore.getLogger({ component: "match" }).warn(
// "bad match rule",
// Logger.E(e)
// );
}
this.cache.set(url, ret);
return ret;
}
protected static getId(val: any): string {
if (typeof val === "object") {
return (<{ uuid: string }>(<unknown>val)).uuid;
}
return <string>(<unknown>val);
}
public del(val: T) {
const id = Match.getId(val);
this.rule.forEach((rules, key) => {
const tmp: T[] = [];
rules.forEach((rule) => {
if (Match.getId(rule) !== id) {
tmp.push(rule);
}
});
if (tmp) {
this.rule.set(key, tmp);
} else {
this.rule.delete(key);
}
});
this.delCache();
}
protected delCache() {
this.cache.clear();
}
return undefined;
}
// 处理油猴的match和include为chrome的matches
export function dealMatches(matches: string[]) {
const result: string[] = [];
for (let i = 0; i < matches.length; i++) {
const url = parseURL(matches[i]);
if (url) {
// *开头但是不是*.的情况
if (url.host.startsWith("*")) {
if (!url.host.startsWith("*.")) {
// 删除开头的*号
url.host = url.host.slice(1);
}
} else if (url.host.endsWith("*")) {
url.host = url.host.slice(0, -1);
export class UrlMatch<T> extends Match<T> {
protected excludeMatch = new Match<T>();
public exclude(url: string, val: T) {
this.excludeMatch.add(url, val);
}
public del(val: T): void {
super.del(val);
this.excludeMatch.del(val);
this.cache.clear();
}
public match(url: string): T[] {
const cache = this.cache.get(url);
if (cache) {
return cache;
}
let ret = super.match(url);
// 排除
const includeMap = new Map();
ret.forEach((val) => {
includeMap.set(Match.getId(val), val);
});
const exclude = this.excludeMatch.match(url);
const excludeMap = new Map();
exclude.forEach((val) => {
excludeMap.set(Match.getId(val), 1);
});
ret = [];
includeMap.forEach((val: T, key) => {
if (!excludeMap.has(key)) {
ret.push(val);
}
});
this.cache.set(url, ret);
return ret;
}
}
export interface PatternMatchesUrl {
scheme: string;
host: string;
path: string;
}
// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理
export function parsePatternMatchesURL(url: string): PatternMatchesUrl | undefined {
let result: PatternMatchesUrl | undefined;
const match = /^(.+?):\/\/(.*?)(\/(.*?)(\?.*?|)|)$/.exec(url);
if (match) {
result = {
scheme: match[1],
host: match[2],
path: match[4] || (url[url.length - 1] === "*" ? "*" : ""),
};
} else {
// 处理一些特殊情况
switch (url) {
case "*":
result = {
scheme: "*",
host: "*",
path: "*",
};
break;
default:
}
}
if (result) {
if (result.host !== "*") {
// *开头但是不是*.的情况
if (result.host.startsWith("*")) {
if (!result.host.startsWith("*.")) {
// 删除开头的*号
result.host = result.host.slice(1);
}
}
// 结尾是*的情况
if (result.host.endsWith("*")) {
result.host = result.host.slice(0, -1);
}
result.push(`${url.scheme}://${url.host}/${url.path}` + (url.search ? "?" + url.search : ""));
}
}
return result;
}
// 处理油猴的match和include为chrome的pattern-matche
export function dealPatternMatches(matches: string[]) {
const patternResult: string[] = [];
const result: string[] = [];
for (let i = 0; i < matches.length; i++) {
const url = parsePatternMatchesURL(matches[i]);
if (url) {
patternResult.push(`${url.scheme}://${url.host}/${url.path}`);
result.push(matches[i]);
}
}
return {
patternResult,
result,
};
}