fix: skip directive matches inside BIND comments
修复 _replace_or_append_option 之前没考虑注释行: - BIND 配置文件里大量 '// ... forwarders ...' 这样的注释 - 原 regex 'forwarders\\b(?!\\-)' 在注释文本里也能匹配到 forwarders - 导致保存时把 forwarders 块插入到了注释行的中间,破坏文件结构 - BIND parser 后续报 'unknown option 223.5.5.5'(行号指向 forwarders 块 里的 IP,因为 forwarders 关键字所在行已被注释污染) 修法:先扫一遍文件建立 in_comment[] 布尔掩码(覆盖 // 行注释、 # 行注释、/* */ 块注释),regex 匹配后检查 match 位置是否在 注释内,若是则 skip。 测试用例包含 '// ... use them as forwarders.' 这种典型注释文本, 验证修改后的 forwarders 块正确插入到 options block 内、不破坏注释。
This commit is contained in:
@@ -1101,24 +1101,70 @@ def _replace_or_append_option(content, directive, new_block):
|
||||
Handles:
|
||||
- brace-delimited: 'forwarders { ... };' / 'allow-recursion { ... };'
|
||||
- value-terminated: 'dnssec-validation auto;' / 'recursion yes;'
|
||||
Nested braces are matched by counting depth.
|
||||
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
|
||||
# whole file linearly:
|
||||
# - '//' to end-of-line: line comment (BIND and shell style)
|
||||
# - '#' to end-of-line: line comment (BIND 9.18+ accepts # too)
|
||||
# - '/* ... */': block comment
|
||||
n = len(content)
|
||||
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)(?!\-)',
|
||||
re.MULTILINE,
|
||||
r'(?<!\w)(?<!-)(' + re.escape(directive) + r')\b(?!-)(?!\w)',
|
||||
)
|
||||
|
||||
for m in pattern.finditer(content):
|
||||
# Skip lines where the directive is part of a longer hyphenated name,
|
||||
# e.g. don't match 'recursion' when looking for it inside 'allow-recursion'.
|
||||
# The negative lookbehind `(?<!\-)` and lookahead `(?!\-)` together cover
|
||||
# both directions. Belt-and-suspenders: also check next char isn't '-'.
|
||||
start = m.start()
|
||||
# Skip if this match is inside a comment
|
||||
if in_comment[start]:
|
||||
continue
|
||||
end_kw = m.end()
|
||||
if end_kw < len(content) and content[end_kw] == '-':
|
||||
continue
|
||||
|
||||
start = m.start()
|
||||
# Detect indentation by looking at whitespace before the directive
|
||||
# Detect indentation
|
||||
line_start = content.rfind('\n', 0, start) + 1
|
||||
indent = ''
|
||||
for ch in content[line_start:start]:
|
||||
@@ -1127,18 +1173,19 @@ def _replace_or_append_option(content, directive, new_block):
|
||||
else:
|
||||
break
|
||||
|
||||
# Look ahead: is this a brace-delimited directive or value-terminated?
|
||||
# Look ahead: brace-delimited or value-terminated?
|
||||
i = end_kw
|
||||
# Skip whitespace
|
||||
while i < len(content) and content[i] in ' \t':
|
||||
i += 1
|
||||
if i >= len(content):
|
||||
continue
|
||||
|
||||
if content[i] == '{':
|
||||
# Brace-delimited: walk forward to matching `};`
|
||||
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
|
||||
@@ -1155,20 +1202,18 @@ def _replace_or_append_option(content, directive, new_block):
|
||||
for line in new_block.split('\n')
|
||||
)
|
||||
return content[:start] + indented + content[end:]
|
||||
continue # malformed, try next match
|
||||
continue
|
||||
else:
|
||||
# Value-terminated: read tokens up to `;`
|
||||
end = i
|
||||
while end < len(content) and content[end] != ';':
|
||||
end += 1
|
||||
if end < len(content):
|
||||
end += 1 # consume the `;`
|
||||
end += 1
|
||||
indented = '\n'.join(
|
||||
(indent + line) if line else line
|
||||
for line in new_block.split('\n')
|
||||
)
|
||||
return content[:start] + indented + content[end:]
|
||||
# else: malformed, try next match
|
||||
|
||||
# Not found — append after the opening "options {" line
|
||||
opt_match = re.search(r'options\s*\{', content)
|
||||
@@ -1180,7 +1225,6 @@ 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' + indented + '\n'
|
||||
return content[:insert_at] + indented + content[insert_at:]
|
||||
# No options block — create one
|
||||
return 'options {\n' + new_block + '\n};\n'
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user