From 9ec9382c1721ad6ea79306679fdd9e445b60c21f Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 24 Aug 2026 13:08:00 +1000 Subject: [PATCH] Cover CJK and four-byte characters in the filename tests No test anywhere under tests/ carried a Han ideograph, kana or Hangul syllable, and the only four-byte character was an emoji in the display sanitizer's provider, so nothing drove one through setName(). The straddle rows are the ones with a branch behind them. forceValidUtf8() matches an incomplete tail with a separate alternative per width, and a byte cut can leave a three-byte character with one or two bytes and a four-byte one with up to three. The single two-byte case that was there exercised one of those alternatives. The rows land on mb_strcut() where ext-mbstring is loaded and on substr() plus the repair in the polyfill job, which runs the mbstring group rather than excluding it and ships no mb_strcut(). Rows 88 and 89 sit in the provider every configuration runs: a character ending on the budget survives a byte cut unchanged, so the expected value holds without the repair. Co-Authored-By: Claude Opus 5 (1M context) --- tests/Upload/FileInfoTest.php | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Upload/FileInfoTest.php b/tests/Upload/FileInfoTest.php index 4b86b83..274c457 100644 --- a/tests/Upload/FileInfoTest.php +++ b/tests/Upload/FileInfoTest.php @@ -155,6 +155,18 @@ public function providerSetNameSanitizingRequiringMbstring(): array between the two bytes of the character at 250. The polyfill ships no `mb_strcut()`, so the cut is a byte cut there and left a partial sequence. */ [str_repeat('a', 249), 'jpeg', str_repeat('a', 249) . "\u{00E9}" . str_repeat('b', 40) . '.jpeg'], + + /* Every offset at which a wider character can straddle that cut. A 3-byte + character can be left with one or two bytes and a 4-byte one with up to three, + and `forceValidUtf8()` matches an incomplete tail with a branch per width — so + one 2-byte case above exercised one of the three. The character is dropped + whole either way: `mb_strcut()` refuses to split it, and on the polyfill the + byte cut splits it and the repair takes the remainder off. */ + [str_repeat('a', 248), 'jpeg', str_repeat('a', 248) . "\u{6587}" . str_repeat('b', 40) . '.jpeg'], + [str_repeat('a', 249), 'jpeg', str_repeat('a', 249) . "\u{6587}" . str_repeat('b', 40) . '.jpeg'], + [str_repeat('a', 247), 'jpeg', str_repeat('a', 247) . "\u{20B9F}" . str_repeat('b', 40) . '.jpeg'], + [str_repeat('a', 248), 'jpeg', str_repeat('a', 248) . "\u{20B9F}" . str_repeat('b', 40) . '.jpeg'], + [str_repeat('a', 249), 'jpeg', str_repeat('a', 249) . "\u{20B9F}" . str_repeat('b', 40) . '.jpeg'], ]; } @@ -277,6 +289,38 @@ public function providerSetNameSanitizing(): array /* Neither is a control character, so both survive */ 75 => ["caf\u{00E9}", 'txt', "caf\u{00E9}.txt"], 76 => ["10\u{20AC}", 'txt', "10\u{20AC}.txt"], + + /* Scripts that carry no ASCII at all, so nothing in the name is left for the + rewriting to key on. The first mixes them with the punctuation it does rewrite, + which is the shape a client name off a CJK desktop actually arrives in. */ + 82 => ["\u{804A}\u{5929}-PC-201711201615", 'xlsx', "\u{804A}\u{5929}(PC)-201711201615.xlsx"], + 83 => ["\u{30D5}\u{30A1}\u{30A4}\u{30EB}\u{540D}", 'txt', "\u{30D5}\u{30A1}\u{30A4}\u{30EB}\u{540D}.txt"], + 84 => ["\u{D30C}\u{C77C}\u{C774}\u{B984}", 'txt', "\u{D30C}\u{C77C}\u{C774}\u{B984}.txt"], + + /* Four-byte characters: CJK Extension B, and the plane the emoji live on. Nothing + above 3 bytes reached the name until these, so the widest sequence the encoding + repair has a branch for was never one this layer had produced. */ + 85 => ["\u{20B9F}\u{2A6B2}", 'txt', "\u{20B9F}\u{2A6B2}.txt"], + 86 => ["photo\u{1F600}", 'png', "photo\u{1F600}.png"], + + /* An extension is ASCII alphanumeric or it is discarded whole, whatever script the + name beside it is in */ + 87 => ["\u{6587}\u{5B57}-v2", '', "\u{6587}\u{5B57}(v2).\u{6587}"], + + /* A character ending exactly on the 250 byte budget is kept whole. The straddling + cases sit in the mbstring provider because only they need the repair; these cut + on a boundary, so a byte cut reaches the same answer and every configuration + runs them. */ + 88 => [ + str_repeat('a', 247) . "\u{6587}", + 'jpeg', + str_repeat('a', 247) . "\u{6587}" . str_repeat('b', 40) . '.jpeg', + ], + 89 => [ + str_repeat('a', 246) . "\u{20B9F}", + 'jpeg', + str_repeat('a', 246) . "\u{20B9F}" . str_repeat('b', 40) . '.jpeg', + ], ]; }