Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion loopx/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,15 @@ def _root_character_atoms(
options = tuple((variant, True) for variant in variants) + tuple(
(variant, True) for variant in encoded_variants
)
# IGNORECASE already covers the one-character case variants. Keeping
# both as alternatives multiplies backtracking at each near-miss letter.
# Multi-character Unicode mappings still need their explicit spelling;
# encoded UTF-8 variants remain distinct byte sequences.
literal_variants = {char} | {variant for variant in variants if len(variant) > 1}
pattern = (
"(?i:"
+ "|".join(
re.escape(variant) for variant in sorted(variants | encoded_variants)
re.escape(variant) for variant in sorted(literal_variants | encoded_variants)
)
+ ")"
)
Expand Down
32 changes: 32 additions & 0 deletions tests/test_chat_path_redaction.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
Expand All @@ -20,6 +21,37 @@
from loopx.chat_status_api import ChatStatusRequestMixin


def test_windows_near_root_finishes_without_exponential_case_backtracking():
# A near miss must reach the generic private-path rule rather than explore
# every equivalent upper/lower-case spelling of the protected prefix. Run
# in a disposable process so a regression cannot stall the whole suite.
result = subprocess.run(
[sys.executable, "-c", """
import json
from loopx.chat import VisibleResponseStreamFilter, redact_local_paths
root = 'Q:/' + 'a' * 32 + 'z'
candidate = 'q:/' + 'a' * 32 + 'y/report.txt'
stream = VisibleResponseStreamFilter(protected_paths=[root])
visible = stream.feed(candidate[:20]) + stream.feed(candidate[20:]) + stream.finish()
print(json.dumps({'batch': redact_local_paths(candidate, protected_paths=[root]), 'stream': visible}))
"""],
cwd=Path(__file__).resolve().parents[1], capture_output=True,
encoding="utf-8", check=True, timeout=20,
)
assert json.loads(result.stdout) == {"batch": "[local-path]", "stream": "[local-path]"}


@pytest.mark.parametrize("component", ["Straße", "İ", "ΐ"])
def test_windows_root_retains_multicharacter_unicode_case_aliases(component):
root = f"Q:/{component}/workspace"
for alias in (component, component.lower(), component.upper()):
candidate = f"q:/{alias}/workspace/report.md"
for spelling in (candidate, quote(candidate, safe="/:")):
assert redact_local_paths(spelling, protected_paths=[root]) == "[project]"
stream = VisibleResponseStreamFilter(protected_paths=[root])
assert stream.feed(spelling[:5]) + stream.feed(spelling[5:]) + stream.finish() == "./report.md"


@pytest.mark.parametrize(
"root",
[
Expand Down
Loading