fix(upload): drop what no name can hold BEFORE the dot rule; a cut never manufactures a kind

Heid bug hunt, hulda, second round on 92c774e:

- A lone surrogate was dropped at the final decode, after the leading-dot
  rule had already run, so "\ud800.forever" came out as .forever, the
  keep marker, and "\ud800.." as "..". The NUL and every unencodable
  character now go first, in one pass, so nothing dropped later can shield
  a dot. Starlette decodes a multipart filename strictly (utf-8, else
  latin-1), so this was not reachable over HTTP; the helper is now right by
  construction regardless.
- A suffix too long to keep was cut like text, and the cut could land on a
  shorter suffix that means something: "….png" out of "….pngxxxx…"
  became an image. A cut that changes classify/doc_kind now has its dots
  neutralised.
- The 16-byte extension threshold was unguarded (every test suffix was 4
  bytes); a .jpeg case pins it.

Falsifiers: tests/mutations/upload_names.toml, 7/7 proved. Not taken here,
as they sit in the upload route rather than this helper: the pickup-id
mkdir outside the try (a FileExistsError race), rmtree(ignore_errors)
hiding a failed cleanup, and a CancelledError skipping cleanup.
This commit is contained in:
vh
2026-09-24 17:04:35 -07:00
parent 92c774e105
commit 225ba32209
3 changed files with 87 additions and 25 deletions
+17
View File
@@ -366,11 +366,28 @@ def test_safe_upload_name_keeps_the_extension_through_the_cut():
# name that USED to fit (80 CJK characters, 240 bytes) must not lose its kind
assert safe_upload_name("é" * 200 + ".png", "fb") == "é" * 98 + ".png"
assert classify(safe_upload_name("画" * 80 + ".png", "fb")) == "image"
# a 5-byte extension is kept as well as a 4-byte one
assert safe_upload_name("é" * 200 + ".jpeg", "fb") == "é" * 97 + ".jpeg"
# an "extension" too long to be one is cut like any other text
long_ext = safe_upload_name("a." + "é" * 150, "fb")
assert len(long_ext.encode("utf-8")) <= 200 and long_ext.startswith("a.é")
def test_safe_upload_name_never_manufactures_a_kind():
# a cut through a long suffix can land on a SHORTER one: `.pngxxx…` is not
# an image, and its cut `….png` would be (heid bug hunt, hulda)
name = safe_upload_name("a" * 196 + ".png" + "x" * 17, "fb")
assert classify(name) == "other" and doc_kind(name) is None
assert name == "a" * 196 + "_png"
def test_safe_upload_name_drops_every_unencodable_character_before_the_dot_rule():
# a lone surrogate is dropped too, and it must go FIRST like the NUL: dropped
# last, it shielded the dot and `.forever` came out, which is the keep marker
assert safe_upload_name("\ud800.forever", "fb") == "forever"
assert safe_upload_name("\ud800..", "fb") == "fb"
def _upload(client, files):
return client.post("/upload", files=files, follow_redirects=False)