修复 _replace_or_append_option 函数的复杂匹配逻辑bug,简化实现确保配置替换正确
This commit is contained in:
@@ -1111,78 +1111,44 @@ def _parse_recursion(content):
|
|||||||
|
|
||||||
|
|
||||||
def _replace_or_append_option(content, directive, new_block):
|
def _replace_or_append_option(content, directive, new_block):
|
||||||
"""Replace an existing 'directive { ... };' or 'directive <value>;' in an
|
"""Replace an existing directive or append it inside the options block.
|
||||||
options block with new_block, or append it after the opening 'options {' if
|
|
||||||
not present.
|
|
||||||
|
|
||||||
`new_block` is the full directive text (without leading/trailing newline).
|
Simple and reliable version:
|
||||||
Handles:
|
1. First try to match brace-delimited directives: 'directive { ... };'
|
||||||
- brace-delimited: 'forwarders { ... };' / 'allow-recursion { ... };'
|
2. Then try to match value-terminated directives: 'directive <value>;'
|
||||||
- value-terminated: 'dnssec-validation auto;' / 'recursion yes;'
|
3. If not found, append after 'options {' opening line
|
||||||
Nested braces are matched by counting depth. Comments ('//', '#', '/*...*/')
|
|
||||||
are skipped so the directive name doesn't match inside comment text.
|
|
||||||
"""
|
"""
|
||||||
# Build a mask of which character ranges are inside comments. We treat the
|
# Pattern 1: brace-delimited (handles nested braces, ignores 'port X' prefixes)
|
||||||
# whole file linearly:
|
# Match: directive [optional port N] { ... };
|
||||||
# - '//' to end-of-line: line comment (BIND and shell style)
|
# The [\w\s.-]* matches things like " port 53"
|
||||||
# - '#' to end-of-line: line comment (BIND 9.18+ accepts # too)
|
pattern_brace = re.compile(
|
||||||
# - '/* ... */': block comment
|
r'(?<!\w)(?<!-)' + re.escape(directive) + r'(?!-)(?!\w)[\w\s.-]*\{',
|
||||||
n = len(content)
|
re.DOTALL
|
||||||
in_comment = [False] * n # True at position i = i is inside a comment
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < n:
|
|
||||||
c = content[i]
|
|
||||||
# End of block comment
|
|
||||||
if in_comment[i] is False and i + 1 < n and c == '/' and content[i + 1] == '*':
|
|
||||||
j = i + 2
|
|
||||||
depth = 1
|
|
||||||
while j < n and depth > 0:
|
|
||||||
if j + 1 < n and content[j] == '*' and content[j + 1] == '/':
|
|
||||||
depth -= 1
|
|
||||||
j += 2
|
|
||||||
else:
|
|
||||||
j += 1
|
|
||||||
for k in range(i, j):
|
|
||||||
in_comment[k] = True
|
|
||||||
i = j
|
|
||||||
continue
|
|
||||||
# Line comment // ... \n
|
|
||||||
if in_comment[i] is False and i + 1 < n and c == '/' and content[i + 1] == '/':
|
|
||||||
j = i
|
|
||||||
while j < n and content[j] != '\n':
|
|
||||||
in_comment[j] = True
|
|
||||||
j += 1
|
|
||||||
i = j
|
|
||||||
continue
|
|
||||||
# Line comment # ... \n (skip only if at start of token; be conservative
|
|
||||||
# and treat any '#' preceded by whitespace or start-of-line as a comment)
|
|
||||||
if in_comment[i] is False and c == '#':
|
|
||||||
# Only treat as comment if preceded by whitespace or start-of-line
|
|
||||||
prev_ok = (i == 0) or content[i - 1] in ' \t'
|
|
||||||
if prev_ok:
|
|
||||||
j = i
|
|
||||||
while j < n and content[j] != '\n':
|
|
||||||
in_comment[j] = True
|
|
||||||
j += 1
|
|
||||||
i = j
|
|
||||||
continue
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
pattern = re.compile(
|
|
||||||
r'(?<!\w)(?<!-)(' + re.escape(directive) + r')\b(?!-)(?!\w)',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for m in pattern.finditer(content):
|
for m in pattern_brace.finditer(content):
|
||||||
start = m.start()
|
start = m.start()
|
||||||
# Skip if this match is inside a comment
|
brace_start = m.end() - 1 # position of '{'
|
||||||
if in_comment[start]:
|
|
||||||
continue
|
|
||||||
end_kw = m.end()
|
|
||||||
if end_kw < len(content) and content[end_kw] == '-':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Detect indentation
|
# Find matching closing brace
|
||||||
|
depth = 1
|
||||||
|
i = brace_start + 1
|
||||||
|
while i < len(content) and depth > 0:
|
||||||
|
if content[i] == '{':
|
||||||
|
depth += 1
|
||||||
|
elif content[i] == '}':
|
||||||
|
depth -= 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if depth == 0:
|
||||||
|
# Found matching }, now find the trailing ;
|
||||||
|
end = i
|
||||||
|
while end < len(content) and content[end] in ' \t\n':
|
||||||
|
end += 1
|
||||||
|
if end < len(content) and content[end] == ';':
|
||||||
|
end += 1
|
||||||
|
|
||||||
|
# Find indentation from original line
|
||||||
line_start = content.rfind('\n', 0, start) + 1
|
line_start = content.rfind('\n', 0, start) + 1
|
||||||
indent = ''
|
indent = ''
|
||||||
for ch in content[line_start:start]:
|
for ch in content[line_start:start]:
|
||||||
@@ -1191,42 +1157,31 @@ def _replace_or_append_option(content, directive, new_block):
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Look ahead: brace-delimited or value-terminated?
|
|
||||||
i = end_kw
|
|
||||||
while i < len(content) and content[i] in ' \t':
|
|
||||||
i += 1
|
|
||||||
if i >= len(content):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if content[i] == '{':
|
|
||||||
depth = 0
|
|
||||||
for j in range(i, len(content)):
|
|
||||||
# Skip over comments inside the brace body
|
|
||||||
if in_comment[j]:
|
|
||||||
continue
|
|
||||||
c = content[j]
|
|
||||||
if c == '{':
|
|
||||||
depth += 1
|
|
||||||
elif c == '}':
|
|
||||||
depth -= 1
|
|
||||||
if depth == 0:
|
|
||||||
end = j + 1
|
|
||||||
while end < len(content) and content[end] in ' \t\n':
|
|
||||||
end += 1
|
|
||||||
if end < len(content) and content[end] == ';':
|
|
||||||
end += 1
|
|
||||||
indented = '\n'.join(
|
indented = '\n'.join(
|
||||||
(indent + line) if line else line
|
(indent + line) if line else line
|
||||||
for line in new_block.split('\n')
|
for line in new_block.split('\n')
|
||||||
)
|
)
|
||||||
return content[:start] + indented + content[end:]
|
return content[:start] + indented + content[end:]
|
||||||
continue
|
|
||||||
|
# Pattern 2: value-terminated (simple values without braces)
|
||||||
|
pattern_value = re.compile(
|
||||||
|
r'(?<!\w)(?<!-)' + re.escape(directive) + r'(?!-)(?!\w)[^;]*;',
|
||||||
|
re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
m = pattern_value.search(content)
|
||||||
|
if m:
|
||||||
|
start = m.start()
|
||||||
|
end = m.end()
|
||||||
|
|
||||||
|
line_start = content.rfind('\n', 0, start) + 1
|
||||||
|
indent = ''
|
||||||
|
for ch in content[line_start:start]:
|
||||||
|
if ch in ' \t':
|
||||||
|
indent += ch
|
||||||
else:
|
else:
|
||||||
end = i
|
break
|
||||||
while end < len(content) and content[end] != ';':
|
|
||||||
end += 1
|
|
||||||
if end < len(content):
|
|
||||||
end += 1
|
|
||||||
indented = '\n'.join(
|
indented = '\n'.join(
|
||||||
(indent + line) if line else line
|
(indent + line) if line else line
|
||||||
for line in new_block.split('\n')
|
for line in new_block.split('\n')
|
||||||
@@ -1243,6 +1198,8 @@ def _replace_or_append_option(content, directive, new_block):
|
|||||||
indented = '\n'.join(' ' + line if line else line for line in new_block.split('\n'))
|
indented = '\n'.join(' ' + line if line else line for line in new_block.split('\n'))
|
||||||
indented = '\n' + indented + '\n'
|
indented = '\n' + indented + '\n'
|
||||||
return content[:insert_at] + indented + content[insert_at:]
|
return content[:insert_at] + indented + content[insert_at:]
|
||||||
|
|
||||||
|
# No options block at all — create one
|
||||||
return 'options {\n' + new_block + '\n};\n'
|
return 'options {\n' + new_block + '\n};\n'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user