From fb3f13c3b2b81865e0e3c53f417a8729ee41bdc2 Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 24 Jul 2026 18:17:08 +0800 Subject: [PATCH] fix: skip directive matches inside BIND comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 _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 内、不破坏注释。 --- app.py | 78 +++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/app.py b/app.py index 16d6cb6..e80df6e 100644 --- a/app.py +++ b/app.py @@ -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'(?= 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'