mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-15 15:50:35 +00:00
fix(gateway): reject artifact archives where a file collides with an implicit directory (#7977) (#8232)
* fix(gateway): reject artifact archives where a file collides with an implicit directory When a run outputs both a file and a deeper file path whose parent directory matches the first file's normalized name (e.g. 'Report' and 'report/slides/lesson.pptx', or Unicode NFC/NFD variations), the ZIP archive builder allowed both entries because it only checked for exact full path equality. On case-insensitive filesystems, extracting such archives fails because the file entry blocks directory creation or vice versa. This change collects all implicit directory prefixes from normalized casefolded entries and rejects the archive with HTTP 409 (ArtifactArchiveError) if any file entry name collides with an implicit directory, regardless of receipt order. Fixes #7977 * fix(gateway): address review feedback on test portability and loop readability
This commit is contained in:
parent
e4b2f4cb9a
commit
9134cf26c6
@ -243,7 +243,15 @@ def build_artifact_archive(
|
||||
reserved = frozenset(name.casefold() for name in {BROWSER_FRAMES_DIRNAME, TOOL_RESULTS_DIRNAME, *extra_reserved_dir_names})
|
||||
members = [_member(root, path, reserved, deadline, root_components) for path in paths]
|
||||
collision_keys = [unicodedata.normalize("NFC", member.entry).casefold() for member in members]
|
||||
if len(collision_keys) != len(set(collision_keys)):
|
||||
collision_set = set(collision_keys)
|
||||
if len(collision_keys) != len(collision_set):
|
||||
raise _reject()
|
||||
implicit_dirs = set()
|
||||
for key in collision_keys:
|
||||
parts = key.split("/")
|
||||
for i in range(1, len(parts)):
|
||||
implicit_dirs.add("/".join(parts[:i]))
|
||||
if collision_set & implicit_dirs:
|
||||
raise _reject()
|
||||
|
||||
sizes = [member.initial.st_size for member in members]
|
||||
|
||||
@ -407,6 +407,74 @@ def test_archive_rejects_casefolded_entry_collisions(tmp_path, monkeypatch) -> N
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"order",
|
||||
[
|
||||
["/mnt/user-data/outputs/Report", "/mnt/user-data/outputs/report/slides/lesson.pptx"],
|
||||
["/mnt/user-data/outputs/report/slides/lesson.pptx", "/mnt/user-data/outputs/Report"],
|
||||
],
|
||||
)
|
||||
def test_archive_rejects_file_colliding_with_implicit_directory(tmp_path, monkeypatch, order) -> None:
|
||||
outputs = tmp_path / "outputs"
|
||||
outputs.mkdir()
|
||||
file_entry = outputs / "Report"
|
||||
file_entry.write_text("file content", encoding="utf-8")
|
||||
lower_report = outputs / "report"
|
||||
if lower_report.exists() and file_entry.samefile(lower_report):
|
||||
pytest.skip("case-sensitive filenames are unavailable on this filesystem")
|
||||
nested_dir = outputs / "report" / "slides"
|
||||
try:
|
||||
nested_dir.mkdir(parents=True)
|
||||
except (NotADirectoryError, FileExistsError):
|
||||
pytest.skip("case-sensitive filenames are unavailable on this filesystem")
|
||||
nested_entry = nested_dir / "lesson.pptx"
|
||||
nested_entry.write_text("slides content", encoding="utf-8")
|
||||
|
||||
client, _, _ = _archive_app(
|
||||
monkeypatch,
|
||||
outputs,
|
||||
paths=order,
|
||||
)
|
||||
|
||||
with client:
|
||||
response = client.post(ARCHIVE_URL)
|
||||
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_archive_rejects_unicode_nfc_nfd_implicit_directory_collision(tmp_path, monkeypatch) -> None:
|
||||
outputs = tmp_path / "outputs"
|
||||
outputs.mkdir()
|
||||
nfc_name = "Caf\u00e9"
|
||||
nfd_dir = "Cafe\u0301"
|
||||
file_entry = outputs / nfc_name
|
||||
file_entry.write_text("cafe content", encoding="utf-8")
|
||||
nfd_path = outputs / nfd_dir
|
||||
if nfd_path.exists() and file_entry.samefile(nfd_path):
|
||||
pytest.skip("normalization-sensitive filenames are unavailable on this filesystem")
|
||||
nested_dir = outputs / nfd_dir
|
||||
try:
|
||||
nested_dir.mkdir(parents=True)
|
||||
except (NotADirectoryError, FileExistsError):
|
||||
pytest.skip("normalization-sensitive filenames are unavailable on this filesystem")
|
||||
nested_entry = nested_dir / "menu.txt"
|
||||
nested_entry.write_text("menu content", encoding="utf-8")
|
||||
|
||||
client, _, _ = _archive_app(
|
||||
monkeypatch,
|
||||
outputs,
|
||||
paths=[
|
||||
f"/mnt/user-data/outputs/{nfc_name}",
|
||||
f"/mnt/user-data/outputs/{nfd_dir}/menu.txt",
|
||||
],
|
||||
)
|
||||
|
||||
with client:
|
||||
response = client.post(ARCHIVE_URL)
|
||||
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_archive_rechecks_size_after_open(tmp_path, monkeypatch) -> None:
|
||||
outputs = tmp_path / "outputs"
|
||||
outputs.mkdir()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user