Skip to content

Commit 241a8df

Browse files
committed
fix: detect portable inventory path aliases
1 parent 5ed1427 commit 241a8df

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

scripts/pg_compat/extract_statements.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import os
77
from pathlib import Path
8+
import unicodedata
89

910
if __package__:
1011
from .common import atomic_write_text, read_jsonl, statement_id
@@ -43,6 +44,13 @@ def _validate_row_object(row, row_index):
4344
raise ValueError(
4445
f"row {row_index}: field name {field!r} must be a string"
4546
)
47+
try:
48+
field.encode("utf-8")
49+
except UnicodeError as error:
50+
raise ValueError(
51+
f"row {row_index}: field name {field!r} is not UTF-8 "
52+
f"encodable: {error}"
53+
) from error
4654

4755

4856
def _validate_required_fields(row, row_index, required_fields):
@@ -273,6 +281,30 @@ def _validate_distinct_paths(input_path, inventory_path, diagnostics_path):
273281
aliases = os.path.samefile(left_path, right_path)
274282
except OSError:
275283
aliases = False
284+
285+
if not aliases:
286+
left_parent = left_resolved.parent
287+
right_parent = right_resolved.parent
288+
same_parent = left_parent == right_parent
289+
if not same_parent:
290+
try:
291+
same_parent = os.path.samefile(
292+
left_parent,
293+
right_parent,
294+
)
295+
except OSError:
296+
same_parent = False
297+
298+
left_name = unicodedata.normalize(
299+
"NFC",
300+
left_resolved.name,
301+
).casefold()
302+
right_name = unicodedata.normalize(
303+
"NFC",
304+
right_resolved.name,
305+
).casefold()
306+
aliases = same_parent and left_name == right_name
307+
276308
if aliases:
277309
raise ValueError(
278310
f"{left_option} and {right_option} must identify distinct files"

tests/pg_compat/test_extract_statements.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,19 @@ def test_rejects_non_string_row_keys_with_context(self):
232232
with self.assertRaisesRegex(ValueError, r"row 0.*field.*1.*string"):
233233
build_inventory([row])
234234

235+
def test_rejects_non_utf8_top_level_field_names_with_context(self):
236+
row = accepted_row()
237+
row["\ud800"] = "invalid key"
238+
239+
with self.assertRaises(ValueError) as raised:
240+
build_inventory([row])
241+
242+
message = str(raised.exception)
243+
self.assertIn("row 0", message)
244+
self.assertIn("field name", message)
245+
self.assertIn("\\ud800", message)
246+
self.assertIn("UTF-8", message)
247+
235248
def test_rejects_non_finite_and_non_utf8_metadata(self):
236249
invalid_metadata = (
237250
(float("nan"), "JSON"),
@@ -467,6 +480,50 @@ def test_rejects_same_path_aliases_before_reading_or_writing(self):
467480
self.assertEqual(input_path.read_text(encoding="utf-8"), "not JSON\n")
468481
self.assertFalse(shared_output_path.exists())
469482

483+
def test_rejects_nonexistent_output_names_differing_only_by_case(self):
484+
with tempfile.TemporaryDirectory() as directory:
485+
directory = Path(directory)
486+
input_path = directory / "raw.jsonl"
487+
inventory_path = directory / "Inventory.jsonl"
488+
diagnostics_path = directory / "inventory.jsonl"
489+
input_path.write_text("", encoding="utf-8")
490+
491+
result = self.run_cli(
492+
"--input",
493+
input_path,
494+
"--inventory",
495+
inventory_path,
496+
"--diagnostics",
497+
diagnostics_path,
498+
)
499+
500+
self.assertNotEqual(result.returncode, 0)
501+
self.assertIn("distinct files", result.stderr)
502+
self.assertFalse(inventory_path.exists())
503+
self.assertFalse(diagnostics_path.exists())
504+
505+
def test_rejects_nonexistent_output_names_with_equivalent_unicode(self):
506+
with tempfile.TemporaryDirectory() as directory:
507+
directory = Path(directory)
508+
input_path = directory / "raw.jsonl"
509+
inventory_path = directory / "caf\u00e9.jsonl"
510+
diagnostics_path = directory / "cafe\u0301.jsonl"
511+
input_path.write_text("", encoding="utf-8")
512+
513+
result = self.run_cli(
514+
"--input",
515+
input_path,
516+
"--inventory",
517+
inventory_path,
518+
"--diagnostics",
519+
diagnostics_path,
520+
)
521+
522+
self.assertNotEqual(result.returncode, 0)
523+
self.assertIn("distinct files", result.stderr)
524+
self.assertFalse(inventory_path.exists())
525+
self.assertFalse(diagnostics_path.exists())
526+
470527
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks are unavailable")
471528
def test_rejects_existing_symlink_path_aliases(self):
472529
with tempfile.TemporaryDirectory() as directory:

0 commit comments

Comments
 (0)