Skip to content

Commit f5854f5

Browse files
joshbouncesecurityar7casper
authored andcommitted
fix: centralize UTF-8 file I/O for Windows compatibility (#45)
1 parent ae1623b commit f5854f5

54 files changed

Lines changed: 754 additions & 437 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

libs/openant-core/context/application_context.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
from anthropic import Anthropic
3333
from dotenv import load_dotenv
34+
from utilities.file_io import open_utf8, read_json, write_json
3435

3536
# Load environment variables
3637
load_dotenv()
@@ -208,7 +209,8 @@ def gather_context_sources(repo_path: Path) -> dict[str, str]:
208209
filepath = repo_path / filename
209210
if filepath.exists():
210211
try:
211-
content = filepath.read_text(errors="ignore")
212+
with open_utf8(filepath, errors="ignore") as _f:
213+
content = _f.read()
212214
# Limit size to avoid token overflow
213215
if len(content) > 10000:
214216
content = content[:10000] + "\n\n[... truncated ...]"
@@ -289,7 +291,8 @@ def detect_entry_points(repo_path: Path) -> str:
289291
continue
290292

291293
try:
292-
content = py_file.read_text(errors="ignore")
294+
with open_utf8(py_file, errors="ignore") as _f:
295+
content = _f.read()
293296
rel_path = py_file.relative_to(repo_path)
294297

295298
for category, patterns in ENTRY_POINT_PATTERNS.items():
@@ -308,7 +311,8 @@ def detect_entry_points(repo_path: Path) -> str:
308311
continue
309312

310313
try:
311-
content = js_file.read_text(errors="ignore")
314+
with open_utf8(js_file, errors="ignore") as _f:
315+
content = _f.read()
312316
rel_path = js_file.relative_to(repo_path)
313317

314318
if re.search(r"express\(\)|require\(['\"]express['\"]\)", content):
@@ -340,15 +344,17 @@ def check_manual_override(repo_path: Path) -> ApplicationContext | None:
340344
continue
341345

342346
try:
343-
content = filepath.read_text()
344-
345347
if filename.endswith('.json'):
346348
# Direct JSON format
347-
data = json.loads(content)
349+
data = read_json(filepath)
348350
data['source'] = 'manual'
349351
return ApplicationContext(**data)
350352

351-
elif filename.endswith('.md'):
353+
# .md files need raw text so regex can extract the embedded JSON block.
354+
with open_utf8(filepath) as _f:
355+
content = _f.read()
356+
357+
if filename.endswith('.md'):
352358
# Markdown format - check for JSON code block
353359
json_match = re.search(r'```json\s*(.*?)\s*```', content, re.DOTALL)
354360
if json_match:
@@ -545,8 +551,7 @@ def save_context(context: ApplicationContext, output_path: Path) -> None:
545551
output_path = Path(output_path)
546552
output_path.parent.mkdir(parents=True, exist_ok=True)
547553

548-
with open(output_path, 'w') as f:
549-
json.dump(asdict(context), f, indent=2)
554+
write_json(output_path, asdict(context))
550555

551556
print(f"Context saved to {output_path}", file=sys.stderr)
552557

@@ -560,9 +565,7 @@ def load_context(input_path: Path) -> ApplicationContext:
560565
Returns:
561566
ApplicationContext loaded from file.
562567
"""
563-
with open(input_path) as f:
564-
data = json.load(f)
565-
568+
data = read_json(input_path)
566569
# Mark as manual to skip validation (already validated when saved)
567570
original_source = data.get('source', 'llm')
568571
data['source'] = 'manual' # Temporarily bypass validation

libs/openant-core/core/analyzer.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
# Import existing analysis machinery
2929
from utilities.llm_client import AnthropicClient, get_global_tracker
30+
from utilities.file_io import read_json, write_json
3031
from utilities.json_corrector import JSONCorrector
3132
from utilities.rate_limiter import get_rate_limiter, is_rate_limit_error, is_retryable_error
3233

@@ -330,9 +331,7 @@ def run_analysis(
330331

331332
# Load dataset
332333
print(f"[Analyze] Loading dataset: {dataset_path}", file=sys.stderr)
333-
with open(dataset_path) as f:
334-
dataset = json.load(f)
335-
334+
dataset = read_json(dataset_path)
336335
units = dataset.get("units", [])
337336

338337
# Diff filter: if upstream parse stamped diff_selected on units (PR-diff
@@ -513,9 +512,7 @@ def _summary_callback(finding, usage=None):
513512
"code_by_route": code_by_route,
514513
}
515514

516-
with open(results_path, "w") as f:
517-
json.dump(experiment_result, f, indent=2)
518-
515+
write_json(results_path, experiment_result)
519516
print(f"\n[Analyze] Results written to {results_path}", file=sys.stderr)
520517

521518
# Checkpoints are preserved as a permanent artifact alongside results.

libs/openant-core/core/checkpoint.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from datetime import datetime, timezone
2828

2929
from utilities.safe_filename import safe_filename
30+
from utilities.file_io import read_json, write_json
3031
from pathlib import Path
3132

3233

@@ -79,8 +80,7 @@ def load(self) -> dict[str, dict]:
7980
continue
8081
filepath = os.path.join(self.dir, filename)
8182
try:
82-
with open(filepath, "r") as f:
83-
data = json.load(f)
83+
data = read_json(filepath)
8484
unit_id = data.get("id")
8585
if unit_id:
8686
results[unit_id] = data
@@ -130,9 +130,7 @@ def save(self, unit_id: str, data: dict):
130130
filename = self._safe_filename(unit_id) + ".json"
131131
filepath = os.path.join(self.dir, filename)
132132
data["id"] = unit_id # ensure id is always present
133-
with open(filepath, "w") as f:
134-
json.dump(data, f, indent=2)
135-
133+
write_json(filepath, data)
136134
def write_summary(
137135
self,
138136
total_units: int,
@@ -168,9 +166,7 @@ def write_summary(
168166
}
169167
if usage is not None:
170168
data["usage"] = usage
171-
with open(filepath, "w") as f:
172-
json.dump(data, f, indent=2)
173-
169+
write_json(filepath, data)
174170
@staticmethod
175171
def read_summary(checkpoint_dir: str) -> dict | None:
176172
"""Read _summary.json from a checkpoint directory.
@@ -182,8 +178,7 @@ def read_summary(checkpoint_dir: str) -> dict | None:
182178
if not os.path.isfile(filepath):
183179
return None
184180
try:
185-
with open(filepath, "r") as f:
186-
return json.load(f)
181+
return read_json(filepath)
187182
except (json.JSONDecodeError, OSError):
188183
return None
189184

@@ -241,8 +236,7 @@ def status(checkpoint_dir: str) -> dict:
241236
continue
242237
filepath = os.path.join(checkpoint_dir, filename)
243238
try:
244-
with open(filepath, "r") as f:
245-
data = json.load(f)
239+
data = read_json(filepath)
246240
except (json.JSONDecodeError, OSError):
247241
errors += 1
248242
error_breakdown["unreadable"] = error_breakdown.get("unreadable", 0) + 1

libs/openant-core/core/diff_filter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030

3131
from __future__ import annotations
3232

33-
import json
3433
import sys
3534
from dataclasses import dataclass, asdict
3635

36+
from utilities.file_io import read_json
37+
3738

3839
# Scope constants (must match internal/git/manifest.go).
3940
SCOPE_CHANGED_FILES = "changed_files"
@@ -65,8 +66,7 @@ def to_dict(self) -> dict:
6566

6667
def load_manifest(path: str) -> dict:
6768
"""Read and minimally validate a diff manifest file."""
68-
with open(path, "r", encoding="utf-8") as f:
69-
m = json.load(f)
69+
m = read_json(path)
7070
scope = m.get("scope")
7171
if scope not in _VALID_SCOPES:
7272
raise ValueError(

libs/openant-core/core/dynamic_tester.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from core.schemas import DynamicTestStepResult, UsageInfo
1414
from core import tracking
15+
from utilities.file_io import read_json, write_json
1516

1617

1718
def run_tests(
@@ -51,9 +52,7 @@ def run_tests(
5152
os.makedirs(output_dir, exist_ok=True)
5253

5354
# Check how many findings to test
54-
with open(pipeline_output_path) as f:
55-
pipeline_data = json.load(f)
56-
55+
pipeline_data = read_json(pipeline_output_path)
5756
findings = pipeline_data.get("findings", [])
5857
testable = [
5958
f for f in findings
@@ -65,8 +64,7 @@ def run_tests(
6564

6665
if not testable:
6766
results_path = os.path.join(output_dir, "dynamic_test_results.json")
68-
with open(results_path, "w") as f:
69-
json.dump({"findings_tested": 0, "results": []}, f, indent=2)
67+
write_json(results_path, {"findings_tested": 0, "results": []})
7068

7169
return DynamicTestStepResult(
7270
results_json_path=results_path,

libs/openant-core/core/enhancer.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from core import tracking
1818
from core.progress import ProgressReporter
1919
from utilities.rate_limiter import configure_rate_limiter
20+
from utilities.file_io import read_json, write_json
2021

2122

2223
def enhance_dataset(
@@ -69,9 +70,7 @@ def enhance_dataset(
6970

7071
# Load dataset
7172
print(f"[Enhance] Loading dataset: {dataset_path}", file=sys.stderr)
72-
with open(dataset_path) as f:
73-
dataset = json.load(f)
74-
73+
dataset = read_json(dataset_path)
7574
units = dataset.get("units", [])
7675
print(f"[Enhance] Units to enhance: {len(units)}", file=sys.stderr)
7776

@@ -138,9 +137,7 @@ def _on_restored(count: int):
138137

139138
# Write enhanced dataset
140139
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
141-
with open(output_path, "w") as f:
142-
json.dump(enhanced, f, indent=2)
143-
140+
write_json(output_path, enhanced)
144141
print(f"[Enhance] Enhanced dataset: {output_path}", file=sys.stderr)
145142
print(f"[Enhance] Classifications: {classifications}", file=sys.stderr)
146143
if error_count:

libs/openant-core/core/parser_adapter.py

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pathlib import Path
1717

1818
from core.schemas import ParseResult
19+
from utilities.file_io import read_json, write_json
1920

2021
# Root of openant-core (where parsers/ lives)
2122
_CORE_ROOT = Path(__file__).parent.parent
@@ -161,9 +162,7 @@ def _maybe_apply_diff_filter(
161162
)
162163
return
163164

164-
with open(result.dataset_path, "r") as f:
165-
dataset = json.load(f)
166-
165+
dataset = read_json(result.dataset_path)
167166
# Dataset may be a dict with "units" or a raw list.
168167
if isinstance(dataset, dict):
169168
units = dataset.get("units", [])
@@ -172,14 +171,11 @@ def _maybe_apply_diff_filter(
172171

173172
stats = apply_diff_filter(units, manifest)
174173

175-
with open(result.dataset_path, "w") as f:
176-
json.dump(dataset, f, indent=2)
177-
174+
write_json(result.dataset_path, dataset)
178175
# Expose stats on the ParseResult via a side-channel file; the parse
179176
# step_context reads this when assembling parse.report.json.
180177
diff_report_path = os.path.join(output_dir, "diff_filter.report.json")
181-
with open(diff_report_path, "w") as f:
182-
json.dump(stats.to_dict(), f, indent=2)
178+
write_json(diff_report_path, stats.to_dict())
183179

184180
print(
185181
f" Diff filter ({stats.scope}): {stats.selected}/{stats.total} units selected"
@@ -245,9 +241,7 @@ def _load_module(name, filename):
245241

246242
print(f"\n[Reachability Filter] Filtering to {processing_level} units...", file=sys.stderr)
247243

248-
with open(call_graph_path, "r") as f:
249-
call_graph_data = json.load(f)
250-
244+
call_graph_data = read_json(call_graph_path)
251245
functions = call_graph_data.get("functions", {})
252246
call_graph = call_graph_data.get("call_graph", {})
253247
reverse_call_graph = call_graph_data.get("reverse_call_graph", {})
@@ -352,12 +346,8 @@ def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_t
352346
dataset = _apply_reachability_filter(dataset, output_dir, processing_level)
353347

354348
# Write outputs
355-
with open(dataset_path, "w") as f:
356-
json.dump(dataset, f, indent=2)
357-
358-
with open(analyzer_output_path, "w") as f:
359-
json.dump(analyzer_output, f, indent=2)
360-
349+
write_json(dataset_path, dataset)
350+
write_json(analyzer_output_path, analyzer_output)
361351
units_count = len(dataset.get("units", []))
362352
print(f" Python parser complete: {units_count} units", file=sys.stderr)
363353

@@ -413,8 +403,7 @@ def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, sk
413403
# Count units
414404
units_count = 0
415405
if os.path.exists(dataset_path):
416-
with open(dataset_path) as f:
417-
data = json.load(f)
406+
data = read_json(dataset_path)
418407
units_count = len(data.get("units", []))
419408

420409
print(f" JavaScript parser complete: {units_count} units", file=sys.stderr)
@@ -470,8 +459,7 @@ def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests
470459
# Count units
471460
units_count = 0
472461
if os.path.exists(dataset_path):
473-
with open(dataset_path) as f:
474-
data = json.load(f)
462+
data = read_json(dataset_path)
475463
units_count = len(data.get("units", []))
476464

477465
print(f" Go parser complete: {units_count} units", file=sys.stderr)
@@ -530,8 +518,7 @@ def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests:
530518
# Count units
531519
units_count = 0
532520
if os.path.exists(dataset_path):
533-
with open(dataset_path) as f:
534-
data = json.load(f)
521+
data = read_json(dataset_path)
535522
units_count = len(data.get("units", []))
536523

537524
print(f" C/C++ parser complete: {units_count} units", file=sys.stderr)
@@ -590,8 +577,7 @@ def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tes
590577
# Count units
591578
units_count = 0
592579
if os.path.exists(dataset_path):
593-
with open(dataset_path) as f:
594-
data = json.load(f)
580+
data = read_json(dataset_path)
595581
units_count = len(data.get("units", []))
596582

597583
print(f" Ruby parser complete: {units_count} units", file=sys.stderr)
@@ -650,8 +636,7 @@ def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_test
650636
# Count units
651637
units_count = 0
652638
if os.path.exists(dataset_path):
653-
with open(dataset_path) as f:
654-
data = json.load(f)
639+
data = read_json(dataset_path)
655640
units_count = len(data.get("units", []))
656641

657642
print(f" PHP parser complete: {units_count} units", file=sys.stderr)
@@ -710,8 +695,7 @@ def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_test
710695
# Count units
711696
units_count = 0
712697
if os.path.exists(dataset_path):
713-
with open(dataset_path) as f:
714-
data = json.load(f)
698+
data = read_json(dataset_path)
715699
units_count = len(data.get("units", []))
716700

717701
print(f" Zig parser complete: {units_count} units", file=sys.stderr)

0 commit comments

Comments
 (0)