Skip to content

Commit 310dbe0

Browse files
committed
fix: text encoding issues
1 parent 384958e commit 310dbe0

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/seclab_taskflows/mcp_servers/local_file_viewer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def search_zipfile(database_path, term, search_dir=None):
6666
continue
6767
with z.open(entry, "r") as f:
6868
for i, line in enumerate(f):
69-
if term in str(line):
69+
if term in line.decode("utf-8", errors="replace"):
7070
filename = remove_root_dir(entry.filename)
7171
if not filename in results:
7272
results[filename] = [i + 1]
@@ -104,7 +104,7 @@ def get_file(database_path, filename):
104104
continue
105105
if remove_root_dir(entry.filename) == filename:
106106
with z.open(entry, "r") as f:
107-
results = [line.rstrip() for line in f]
107+
results = [line.decode("utf-8", errors="replace").rstrip() for line in f]
108108
return results
109109
return results
110110

tests/test_local_file_viewer.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# SPDX-FileCopyrightText: GitHub, Inc.
2+
# SPDX-License-Identifier: MIT
3+
4+
import os
5+
import tempfile
6+
import zipfile
7+
from pathlib import Path
8+
from unittest.mock import patch
9+
10+
import pytest
11+
12+
import seclab_taskflows.mcp_servers.local_file_viewer as lfv_mod
13+
from seclab_taskflows.mcp_servers.local_file_viewer import get_file, search_zipfile
14+
15+
# The zip entries use a root directory prefix, like a GitHub zipball, which
16+
# local_file_viewer strips via remove_root_dir().
17+
ROOT = "owner-repo-abc1234"
18+
19+
SAMPLE = 'import os\nprint("héllo wörld")\n'
20+
21+
22+
def _make_zip_on_disk(tmp_dir, owner, repo, files):
23+
"""Write a zipball-style archive to {tmp_dir}/{owner}/{repo}.zip."""
24+
owner_dir = Path(tmp_dir) / owner
25+
owner_dir.mkdir(parents=True, exist_ok=True)
26+
zip_path = owner_dir / f"{repo}.zip"
27+
with zipfile.ZipFile(zip_path, "w") as zf:
28+
for path, content in files.items():
29+
zf.writestr(f"{ROOT}/{path}", content)
30+
return zip_path
31+
32+
33+
class TestGetFileDecodesBytes:
34+
def test_get_file_returns_decoded_str_lines(self):
35+
with tempfile.TemporaryDirectory() as tmp_dir:
36+
zip_path = _make_zip_on_disk(tmp_dir, "owner", "repo", {"foo.py": SAMPLE})
37+
lines = get_file(zip_path, "foo.py")
38+
# Lines must be decoded str, not bytes, and free of b'...' reprs.
39+
assert lines == ["import os", 'print("héllo wörld")']
40+
assert all(isinstance(line, str) for line in lines)
41+
42+
def test_get_file_handles_invalid_utf8_without_crashing(self):
43+
with tempfile.TemporaryDirectory() as tmp_dir:
44+
owner_dir = Path(tmp_dir) / "owner"
45+
owner_dir.mkdir(parents=True)
46+
zip_path = owner_dir / "repo.zip"
47+
with zipfile.ZipFile(zip_path, "w") as zf:
48+
zf.writestr(f"{ROOT}/bin.dat", b"good\n\xff\xfe\n")
49+
lines = get_file(zip_path, "bin.dat")
50+
assert lines[0] == "good"
51+
assert all(isinstance(line, str) for line in lines)
52+
53+
54+
class TestSearchZipfileDecodesBytes:
55+
def test_search_matches_non_ascii_term(self):
56+
with tempfile.TemporaryDirectory() as tmp_dir:
57+
zip_path = _make_zip_on_disk(tmp_dir, "owner", "repo", {"foo.py": SAMPLE})
58+
results = search_zipfile(zip_path, "héllo")
59+
assert results == {"foo.py": [2]}
60+
61+
62+
class TestFetchFileContentTool:
63+
@pytest.mark.asyncio
64+
async def test_fetch_file_content_returns_decoded_numbered_lines(self):
65+
with tempfile.TemporaryDirectory() as tmp_dir:
66+
_make_zip_on_disk(tmp_dir, "owner", "repo", {"foo.py": SAMPLE})
67+
with patch.object(lfv_mod, "LOCAL_GH_DIR", tmp_dir):
68+
result = await lfv_mod.fetch_file_content(owner="Owner", repo="Repo", path="foo.py")
69+
assert "1: import os" in result
70+
assert '2: print("héllo wörld")' in result
71+
# Ensure no Python bytes reprs leak into the output.
72+
assert "b'" not in result
73+
74+
@pytest.mark.asyncio
75+
async def test_get_file_lines_returns_decoded_range(self):
76+
with tempfile.TemporaryDirectory() as tmp_dir:
77+
_make_zip_on_disk(tmp_dir, "owner", "repo", {"foo.py": SAMPLE})
78+
with patch.object(lfv_mod, "LOCAL_GH_DIR", tmp_dir):
79+
result = await lfv_mod.get_file_lines(
80+
owner="owner", repo="repo", path="foo.py", start_line=2, length=1
81+
)
82+
assert result == '2: print("héllo wörld")'
83+
assert "b'" not in result
84+
85+
86+
if __name__ == "__main__":
87+
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)