-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Expand file tree
/
Copy pathtag.ts
More file actions
84 lines (75 loc) · 2.84 KB
/
Copy pathtag.ts
File metadata and controls
84 lines (75 loc) · 2.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import type { UserSetting_TagMetadata, UserSetting_TagsSetting } from "@/types/proto/api/v1/user_service_pb";
// Cache compiled regexes to avoid re-compiling on every tag render.
const compiledPatternCache = new Map<string, RegExp | null>();
/** Merge exact tag counts without inheriting Object prototype keys. */
export const mergeTagCounts = (...sources: Array<Record<string, number> | undefined>): Record<string, number> => {
const result = Object.create(null) as Record<string, number>;
for (const source of sources) {
for (const [tag, count] of Object.entries(source ?? {})) result[tag] = (result[tag] ?? 0) + count;
}
return result;
};
const getCompiledPattern = (pattern: string): RegExp | null => {
if (compiledPatternCache.has(pattern)) {
return compiledPatternCache.get(pattern)!;
}
let re: RegExp | null = null;
try {
re = new RegExp(`^(?:${pattern})$`);
} catch {
// Invalid pattern — cache as null so we skip it without retrying.
}
compiledPatternCache.set(pattern, re);
return re;
};
const matchesTagPattern = (tag: string, pattern: string, re: RegExp): boolean => {
if (re.test(tag)) {
return true;
}
// A tag hierarchy exposes its prefixes as tags too. Treat a trailing /.*
// rule as matching the prefix itself by testing the empty suffix.
return pattern.endsWith("/.*") && re.test(`${tag}/`);
};
/**
* Finds the first matching TagMetadata for a given tag name by treating each
* key in tagsSetting.tags as an anchored regex pattern (^pattern$). A trailing
* /.* pattern also matches its hierarchy prefix.
*
* Lookup order:
* 1. Exact key match (O(1) fast path, backward-compatible).
* 2. Iterate all keys and test as anchored regex — first match wins.
*/
export const findTagMetadata = (tag: string, tagsSetting: UserSetting_TagsSetting): UserSetting_TagMetadata | undefined => {
// Fast path: exact match.
if (Object.hasOwn(tagsSetting.tags, tag)) {
return tagsSetting.tags[tag];
}
// Regex path: treat each key as an anchored pattern.
for (const [pattern, metadata] of Object.entries(tagsSetting.tags)) {
const re = getCompiledPattern(pattern);
if (re && matchesTagPattern(tag, pattern, re)) {
return metadata;
}
}
return undefined;
};
/**
* Returns true if the given string is a valid, ReDoS-safe JavaScript regex pattern.
*
* Rejects patterns with nested quantifiers (e.g. `(a+)+`) which can cause
* catastrophic backtracking in JavaScript's regex engine.
*/
export const isValidTagPattern = (pattern: string): boolean => {
if (!pattern) return false;
try {
new RegExp(pattern);
} catch {
return false;
}
// Reject nested quantifiers: a quantified group whose body itself contains
// a quantifier — the classic ReDoS shape e.g. (a+)+, (a*b?)+, (x|y+)+.
if (/\((?:[^()]*[*+?{][^()]*)\)[*+?{]/.test(pattern)) {
return false;
}
return true;
};