forked from 777genius/claude-code-source-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshellRuleMatching.ts
More file actions
229 lines (207 loc) · 6.26 KB
/
Copy pathshellRuleMatching.ts
File metadata and controls
229 lines (207 loc) · 6.26 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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/**
* Shared permission rule matching utilities for shell tools.
*
* Extracts common logic for:
* - Parsing permission rules (exact, prefix, wildcard)
* - Matching commands against rules
* - Generating permission suggestions
*/
import type { PermissionUpdate } from './PermissionUpdateSchema.js'
// Null-byte sentinel placeholders for wildcard pattern escaping — module-level
// so the RegExp objects are compiled once instead of per permission check.
const ESCAPED_STAR_PLACEHOLDER = '\x00ESCAPED_STAR\x00'
const ESCAPED_BACKSLASH_PLACEHOLDER = '\x00ESCAPED_BACKSLASH\x00'
const ESCAPED_STAR_PLACEHOLDER_RE = new RegExp(ESCAPED_STAR_PLACEHOLDER, 'g')
const ESCAPED_BACKSLASH_PLACEHOLDER_RE = new RegExp(
ESCAPED_BACKSLASH_PLACEHOLDER,
'g',
)
/**
* Parsed permission rule discriminated union.
*/
export type ShellPermissionRule =
| {
type: 'exact'
command: string
}
| {
type: 'prefix'
prefix: string
}
| {
type: 'wildcard'
pattern: string
}
/**
* Extract prefix from legacy :* syntax (e.g., "npm:*" -> "npm")
* This is maintained for backwards compatibility.
*/
export function permissionRuleExtractPrefix(
permissionRule: string,
): string | null {
const match = permissionRule.match(/^(.+):\*$/)
return match?.[1] ?? null
}
/**
* Check if a pattern contains unescaped wildcards (not legacy :* syntax).
* Returns true if the pattern contains * that are not escaped with \ or part of :* at the end.
*/
export function hasWildcards(pattern: string): boolean {
// If it ends with :*, it's legacy prefix syntax, not wildcard
if (pattern.endsWith(':*')) {
return false
}
// Check for unescaped * anywhere in the pattern
// An asterisk is unescaped if it's not preceded by a backslash,
// or if it's preceded by an even number of backslashes (escaped backslashes)
for (let i = 0; i < pattern.length; i++) {
if (pattern[i] === '*') {
// Count backslashes before this asterisk
let backslashCount = 0
let j = i - 1
while (j >= 0 && pattern[j] === '\\') {
backslashCount++
j--
}
// If even number of backslashes (including 0), the asterisk is unescaped
if (backslashCount % 2 === 0) {
return true
}
}
}
return false
}
/**
* Match a command against a wildcard pattern.
* Wildcards (*) match any sequence of characters.
* Use \* to match a literal asterisk character.
* Use \\ to match a literal backslash.
*
* @param pattern - The permission rule pattern with wildcards
* @param command - The command to match against
* @returns true if the command matches the pattern
*/
export function matchWildcardPattern(
pattern: string,
command: string,
caseInsensitive = false,
): boolean {
// Trim leading/trailing whitespace from pattern
const trimmedPattern = pattern.trim()
// Process the pattern to handle escape sequences: \* and \\
let processed = ''
let i = 0
while (i < trimmedPattern.length) {
const char = trimmedPattern[i]
// Handle escape sequences
if (char === '\\' && i + 1 < trimmedPattern.length) {
const nextChar = trimmedPattern[i + 1]
if (nextChar === '*') {
// \* -> literal asterisk placeholder
processed += ESCAPED_STAR_PLACEHOLDER
i += 2
continue
} else if (nextChar === '\\') {
// \\ -> literal backslash placeholder
processed += ESCAPED_BACKSLASH_PLACEHOLDER
i += 2
continue
}
}
processed += char
i++
}
// Escape regex special characters except *
const escaped = processed.replace(/[.+?^${}()|[\]\\'"]/g, '\\$&')
// Convert unescaped * to .* for wildcard matching
const withWildcards = escaped.replace(/\*/g, '.*')
// Convert placeholders back to escaped regex literals
let regexPattern = withWildcards
.replace(ESCAPED_STAR_PLACEHOLDER_RE, '\\*')
.replace(ESCAPED_BACKSLASH_PLACEHOLDER_RE, '\\\\')
// When a pattern ends with ' *' (space + unescaped wildcard) AND the trailing
// wildcard is the ONLY unescaped wildcard, make the trailing space-and-args
// optional so 'git *' matches both 'git add' and bare 'git'.
// This aligns wildcard matching with prefix rule semantics (git:*).
// Multi-wildcard patterns like '* run *' are excluded — making the last
// wildcard optional would incorrectly match 'npm run' (no trailing arg).
const unescapedStarCount = (processed.match(/\*/g) || []).length
if (regexPattern.endsWith(' .*') && unescapedStarCount === 1) {
regexPattern = regexPattern.slice(0, -3) + '( .*)?'
}
// Create regex that matches the entire string.
// The 's' (dotAll) flag makes '.' match newlines, so wildcards match
// commands containing embedded newlines (e.g. heredoc content after splitCommand_DEPRECATED).
const flags = 's' + (caseInsensitive ? 'i' : '')
const regex = new RegExp(`^${regexPattern}$`, flags)
return regex.test(command)
}
/**
* Parse a permission rule string into a structured rule object.
*/
export function parsePermissionRule(
permissionRule: string,
): ShellPermissionRule {
// Check for legacy :* prefix syntax first (backwards compatibility)
const prefix = permissionRuleExtractPrefix(permissionRule)
if (prefix !== null) {
return {
type: 'prefix',
prefix,
}
}
// Check for new wildcard syntax (contains * but not :* at end)
if (hasWildcards(permissionRule)) {
return {
type: 'wildcard',
pattern: permissionRule,
}
}
// Otherwise, it's an exact match
return {
type: 'exact',
command: permissionRule,
}
}
/**
* Generate permission update suggestion for an exact command match.
*/
export function suggestionForExactCommand(
toolName: string,
command: string,
): PermissionUpdate[] {
return [
{
type: 'addRules',
rules: [
{
toolName,
ruleContent: command,
},
],
behavior: 'allow',
destination: 'localSettings',
},
]
}
/**
* Generate permission update suggestion for a prefix match.
*/
export function suggestionForPrefix(
toolName: string,
prefix: string,
): PermissionUpdate[] {
return [
{
type: 'addRules',
rules: [
{
toolName,
ruleContent: `${prefix}:*`,
},
],
behavior: 'allow',
destination: 'localSettings',
},
]
}