Skip to content

Commit 7c19d6b

Browse files
committed
Simplify URL matching (no RegExps)
1 parent 7019d79 commit 7c19d6b

5 files changed

Lines changed: 130 additions & 196 deletions

File tree

src/config/dynamic-theme-fixes.config

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ CSS
885885
================================
886886

887887
aliexpress.*
888+
aliexpress.*.*
888889

889890
CSS
890891
a[class^="SnowOrderDetails_Product__imageWrap"],
@@ -3499,6 +3500,7 @@ td:has(img[alt=QR]) {
34993500
================================
35003501

35013502
books.google.*
3503+
books.google.*.*
35023504

35033505
INVERT
35043506
.jfk-button-img
@@ -15547,6 +15549,7 @@ CSS
1554715549
================================
1554815550

1554915551
music.amazon.*
15552+
music.amazon.*.*
1555015553

1555115554
CSS
1555215555
.listViewStatusButtonInLibrary .add,
@@ -18054,7 +18057,9 @@ INVERT
1805418057

1805518058
================================
1805618059

18057-
polarion*
18060+
polarion.*
18061+
polarion.*.*
18062+
polarion.*.*.*
1805818063

1805918064
INVERT
1806018065
.polarion-dle-toolbar-Button img
@@ -18904,6 +18909,7 @@ INVERT
1890418909
================================
1890518910

1890618911
read.amazon.*
18912+
read.amazon.*.*
1890718913
lire.amazon.*
1890818914

1890918915
INVERT
@@ -18990,6 +18996,7 @@ INVERT
1899018996
================================
1899118997

1899218998
redcross.*
18999+
redcross.*.*
1899319000

1899419001
CSS
1899519002
.lecture-attachment img.is-loaded {
@@ -21341,6 +21348,7 @@ a[href="//stooq.pl"] path:not([fill]) {
2134121348
================================
2134221349

2134321350
store.google.*
21351+
store.google.*.*
2134421352

2134521353
CSS
2134621354
[style*="background-image"] {

src/utils/cache.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function cachedFactory<K, V>(factory: (key: K) => V, size: number): (key: K) => V {
2+
const cache = new Map<K, V>();
3+
4+
return (key: K) => {
5+
if (cache.has(key)) {
6+
return cache.get(key)!;
7+
}
8+
const value = factory(key);
9+
cache.set(key, value);
10+
if (cache.size > size) {
11+
const first = cache.keys().next().value;
12+
cache.delete(first);
13+
}
14+
return value;
15+
};
16+
}

src/utils/url.ts

Lines changed: 78 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {UserSettings, TabInfo} from '../definitions';
2+
import {cachedFactory} from './cache';
23
import {isIPV6, compareIPV6} from './ipv6';
34

45
declare const __THUNDERBIRD__: boolean;
@@ -108,97 +109,102 @@ export function isURLMatched(url: string, urlTemplate: string): boolean {
108109
if (isFirstIPV6 && isSecondIPV6) {
109110
return compareIPV6(url, urlTemplate);
110111
} else if (!isFirstIPV6 && !isSecondIPV6) {
111-
let regex: RegExp;
112-
try {
113-
regex = createCachedURLRegex(urlTemplate);
114-
} catch (e) {
115-
return false;
116-
}
117-
return Boolean(url.match(regex));
112+
return matchURLPattern(url, urlTemplate);
118113
}
119114
return false;
120115
}
121116

122-
const URL_MATCH_CACHE_SIZE = 32 * 1024;
123-
const urlMatchCache = new Map<string, RegExp>();
124-
125-
function createCachedURLRegex(urlTemplate: string): RegExp {
126-
if (urlMatchCache.has(urlTemplate)) {
127-
return urlMatchCache.get(urlTemplate)!;
117+
const URL_CACHE_SIZE = 32;
118+
const prepareURL = cachedFactory((url: string) => {
119+
let parsed: URL;
120+
try {
121+
parsed = new URL(url);
122+
} catch (err) {
123+
return null;
128124
}
129-
130-
const regex = createURLRegex(urlTemplate);
131-
urlMatchCache.set(urlTemplate, regex);
132-
if (urlMatchCache.size > URL_MATCH_CACHE_SIZE) {
133-
const first = urlMatchCache.keys().next().value;
134-
urlMatchCache.delete(first);
125+
const host = parsed.host;
126+
const path = parsed.pathname;
127+
const hostParts = host.split('.').reverse();
128+
const pathParts = path.split('/').slice(1);
129+
if (!pathParts[pathParts.length - 1]) {
130+
pathParts.splice(pathParts.length - 1, 1);
135131
}
136-
return regex;
137-
}
138-
139-
function createURLRegex(urlTemplate: string): RegExp {
140-
urlTemplate = urlTemplate.trim();
141-
const exactBeginning = (urlTemplate[0] === '^');
142-
const exactEnding = (urlTemplate[urlTemplate.length - 1] === '$');
143-
const hasLastSlash = /\/\$?$/.test(urlTemplate);
144-
145-
urlTemplate = (urlTemplate
146-
.replace(/^\^/, '') // Remove ^ at start
147-
.replace(/\$$/, '') // Remove $ at end
148-
.replace(/^.*?\/{2,3}/, '') // Remove scheme
149-
.replace(/\?.*$/, '') // Remove query
150-
.replace(/\/$/, '') // Remove last slash
151-
);
132+
return {
133+
hostParts,
134+
pathParts,
135+
};
136+
}, URL_CACHE_SIZE);
152137

153-
let slashIndex: number;
154-
let beforeSlash: string;
155-
let afterSlash: string | undefined;
156-
if ((slashIndex = urlTemplate.indexOf('/')) >= 0) {
157-
beforeSlash = urlTemplate.substring(0, slashIndex); // google.*
158-
afterSlash = urlTemplate.replace(/\$/g, '').substring(slashIndex); // /login/abc
159-
} else {
160-
beforeSlash = urlTemplate.replace(/\$/g, '');
138+
const URL_MATCH_CACHE_SIZE = 32 * 1024;
139+
const preparePattern = cachedFactory((pattern: string) => {
140+
if (!pattern) {
141+
return null;
161142
}
162143

163-
//
164-
// SCHEME and SUBDOMAINS
144+
const exactStart = pattern.startsWith('^');
145+
const exactEnd = pattern.endsWith('$');
146+
if (exactStart) {
147+
pattern = pattern.substring(1);
148+
}
149+
if (exactEnd) {
150+
pattern = pattern.substring(0, pattern.length - 1);
151+
}
165152

166-
let result = (exactBeginning ?
167-
'^(.*?\\:\\/{2,3})?' // Scheme
168-
: '^(.*?\\:\\/{2,3})?([^\/]*?\\.)?' // Scheme and subdomains
169-
);
153+
const slashIndex = pattern.indexOf('/');
154+
const host = slashIndex < 0 ? pattern : pattern.substring(0, slashIndex);
155+
const path = slashIndex < 0 ? '' : pattern.substring(slashIndex + 1);
156+
const hostParts = host.split('.').reverse();
157+
const pathParts = path.split('/');
158+
if (!pathParts[pathParts.length - 1]) {
159+
pathParts.splice(pathParts.length - 1, 1);
160+
}
170161

171-
//
172-
// HOST and PORT
162+
return {
163+
hostParts,
164+
pathParts,
165+
exactStart,
166+
exactEnd,
167+
};
168+
}, URL_MATCH_CACHE_SIZE);
169+
170+
function matchURLPattern(url: string, pattern: string) {
171+
const u = prepareURL(url);
172+
const p = preparePattern(pattern);
173+
174+
if (
175+
!(u && p)
176+
|| (p.hostParts.length > u.hostParts.length)
177+
|| (p.exactStart && p.hostParts.length !== u.hostParts.length)
178+
|| (p.exactEnd && p.pathParts.length !== u.pathParts.length)
179+
) {
180+
return false;
181+
}
173182

174-
const hostParts = beforeSlash.split('.');
175-
result += '(';
176-
for (let i = 0; i < hostParts.length; i++) {
177-
if (hostParts[i] === '*') {
178-
hostParts[i] = '[^\\.\\/]+?';
183+
for (let i = 0; i < p.hostParts.length; i++) {
184+
const pHostPart = p.hostParts[i];
185+
const uHostPart = u.hostParts[i];
186+
if (pHostPart !== '*' && pHostPart !== uHostPart) {
187+
return false;
179188
}
180189
}
181-
result += hostParts.join('\\.');
182-
result += ')';
183-
184-
//
185-
// PATH and QUERY
186190

187-
if (afterSlash) {
188-
result += '(';
189-
result += afterSlash.replace('/', '\\/');
190-
result += ')';
191+
if (p.pathParts.length === 0) {
192+
return true;
191193
}
192194

193-
result += (exactEnding ?
194-
'(\\/?(\\?[^\/]*?)?)$' // All following queries
195-
: `(\\/${hasLastSlash ? '' : '?'}.*?)$` // All following paths and queries
196-
);
195+
if (p.pathParts.length > u.pathParts.length) {
196+
return false;
197+
}
197198

198-
//
199-
// Result
199+
for (let i = 0; i < p.pathParts.length; i++) {
200+
const pPathPart = p.pathParts[i];
201+
const uPathPart = u.pathParts[i];
202+
if (pPathPart !== '*' && pPathPart !== uPathPart) {
203+
return false;
204+
}
205+
}
200206

201-
return new RegExp(result, 'i');
207+
return true;
202208
}
203209

204210
export function isPDF(url: string): boolean {

tests/unit/generators/utils/parse.tests.ts

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -448,128 +448,6 @@ describe('Explicit wildcard domain patterns', () => {
448448
});
449449
});
450450

451-
describe('Backwards compatibility', () => {
452-
describe('Nonstandard patterns', () => {
453-
it('Clearly non-matching pattern', () => {
454-
interface TestFix {
455-
url: string[];
456-
directive: string;
457-
}
458-
459-
const directiveMap: { [key: string]: keyof TestFix } = {
460-
DIRECTIVE: 'directive',
461-
};
462-
463-
const config = [
464-
'*',
465-
'',
466-
'DIRECTIVE',
467-
'hello world',
468-
'',
469-
'====================',
470-
'',
471-
'example*.com',
472-
'',
473-
'DIRECTIVE',
474-
'one',
475-
'',
476-
].join('\n');
477-
478-
const options: SitesFixesParserOptions<TestFix> = {
479-
commands: Object.keys(directiveMap),
480-
getCommandPropName: (command) => directiveMap[command],
481-
parseCommandValue: (_, value) => value.trim(),
482-
};
483-
const index = indexSitesFixesConfig<TestFix>(config);
484-
485-
const fixes = getSitesFixesFor<TestFix>('other.net', config, index, options);
486-
expect(fixes).toEqual([
487-
{
488-
'url': ['*'],
489-
'directive': 'hello world',
490-
},
491-
]);
492-
});
493-
494-
it('Legacy matching pattern', () => {
495-
interface TestFix {
496-
url: string[];
497-
directive: string;
498-
}
499-
500-
const directiveMap: { [key: string]: keyof TestFix } = {
501-
DIRECTIVE: 'directive',
502-
};
503-
504-
const config = [
505-
'*',
506-
'',
507-
'DIRECTIVE',
508-
'hello world',
509-
'',
510-
'====================',
511-
'',
512-
'example*',
513-
'',
514-
'DIRECTIVE',
515-
'one',
516-
'',
517-
].join('\n');
518-
519-
const options: SitesFixesParserOptions<TestFix> = {
520-
commands: Object.keys(directiveMap),
521-
getCommandPropName: (command) => directiveMap[command],
522-
parseCommandValue: (_, value) => value.trim(),
523-
};
524-
const index = indexSitesFixesConfig<TestFix>(config);
525-
526-
const fixes = getSitesFixesFor<TestFix>('example.com', config, index, options);
527-
expect(fixes).toEqual([
528-
{
529-
'url': ['*'],
530-
'directive': 'hello world',
531-
},
532-
{
533-
'url': ['example*'],
534-
'directive': 'one',
535-
},
536-
]);
537-
538-
const fixes2 = getSitesFixesFor<TestFix>('example.deep.com', config, index, options);
539-
expect(fixes2).toEqual([
540-
{
541-
'url': ['*'],
542-
'directive': 'hello world',
543-
},
544-
{
545-
'url': ['example*'],
546-
'directive': 'one',
547-
},
548-
]);
549-
550-
const fixes3 = getSitesFixesFor<TestFix>('nonexample.com', config, index, options);
551-
expect(fixes3).toEqual([
552-
{
553-
'url': ['*'],
554-
'directive': 'hello world',
555-
},
556-
]);
557-
558-
const fixes4 = getSitesFixesFor<TestFix>('deep.example.com', config, index, options);
559-
expect(fixes4).toEqual([
560-
{
561-
'url': ['*'],
562-
'directive': 'hello world',
563-
},
564-
{
565-
'url': ['example*'],
566-
'directive': 'one',
567-
},
568-
]);
569-
});
570-
});
571-
});
572-
573451
test('Implied wildcards', () => {
574452
interface TestFix {
575453
url: string[];

0 commit comments

Comments
 (0)