Add regression tests for checking that decomposed catalog names get canonically reordered and legacy-decomposition corrected before storage, matching what macOS's own fsck_hfs requires and previously flagged as "Illegal name" (reproducible via xfstests generic/339). Two small helpers, test_char2uni_utf8()/test_uni2char_utf8(), wrap the real utf8_to_utf32()/utf32_to_utf8() codec: the existing test_char2uni()/test_uni2char() stand-ins only handle one raw byte per character and can't represent the multi-byte combining marks (U+0301, U+0323, ...) these tests need. New cases: - hfsplus_asc2uni_combining_reorder_test: two independently-typed combining marks (U+0301 class 230, U+0323 class 220) end up stored in ascending combining-class order regardless of typed order, are left alone when already canonical, and keep their relative order when their classes are equal (the sort must be stable). - hfsplus_unicode_combining_reorder_roundtrip_test: a name stored with marks reordered still reads back as the same, valid UTF-8. - hfsplus_asc2uni_legacy_decomp_test: U+01F8 decomposes to "N" + U+0300 even though Apple Technote #1150's table has no entry for it. - hfsplus_asc2uni_legacy_seq_fixup_test: a Greek letter + U+030D becomes that letter + U+0301, and Bengali BA + NUKTA collapses to the single letter RA WITH MIDDLE DIAGONAL. - hfsplus_hash_dentry_combining_reorder_test: two names differing only in typed mark order hash identically, since both canonicalize to the same stored form. - hfsplus_compare_dentry_combining_reorder_test and hfsplus_compare_dentry_legacy_decomp_test: likewise, both compare equal - a precomposed U+01F8 and its already-decomposed "N" + U+0300 spelling must be found as the same catalog entry. All 34 cases in the hfsplus_unicode KUnit suite (27 existing + 7 new) pass under tools/testing/kunit/kunit.py. Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/unicode_test.c | 308 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) diff --git a/fs/hfsplus/unicode_test.c b/fs/hfsplus/unicode_test.c index 32a64029d77b..a683c6d5fc22 100644 --- a/fs/hfsplus/unicode_test.c +++ b/fs/hfsplus/unicode_test.c @@ -948,6 +948,223 @@ static void hfsplus_asc2uni_decompose_test(struct kunit *test) free_mock_sb(mock_sb); } +/* + * Real UTF-8 <-> wchar_t conversion, mirroring what the "utf8" NLS table + * actually does (see char2uni()/uni2char() in fs/nls/nls_utf8.c). The + * test_char2uni()/test_uni2char() stand-ins above only handle one raw + * byte per character, which cannot represent the combining marks (e.g. + * U+0301, U+0323) the canonical-reordering tests below need. + */ +static int test_char2uni_utf8(const unsigned char *rawstring, int boundlen, + wchar_t *uni) +{ + unicode_t u; + int n = utf8_to_utf32(rawstring, boundlen, &u); + + if (n < 0 || u > MAX_WCHAR_T) { + *uni = 0x3f; /* ? */ + return -EINVAL; + } + *uni = (wchar_t)u; + return n; +} + +static int test_uni2char_utf8(wchar_t uni, unsigned char *out, int boundlen) +{ + int n = utf32_to_utf8(uni, out, boundlen); + + if (n < 0) { + *out = '?'; + return -EINVAL; + } + return n; +} + +/* + * Test that hfsplus_asc2uni() brings combining marks contributed by + * different source characters into Unicode canonical (combining-class) + * order, instead of just storing them in whatever order they were typed. + * + * U+0301 (COMBINING ACUTE ACCENT) has combining class 230; U+0323 + * (COMBINING DOT BELOW) has combining class 220. Since 220 < 230, + * canonical order requires the dot-below before the acute. A name + * created with them typed in the "wrong" order must still end up stored + * in canonical order - this is exactly what macOS's own fsck_hfs + * (FixDecomps() in CatalogCheck.c) requires and flags as "Illegal name" + * when it isn't true. + */ +static void hfsplus_asc2uni_combining_reorder_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + /* "e" + COMBINING ACUTE ACCENT (U+0301) + COMBINING DOT BELOW (U+0323) */ + static const char wrong_order[] = "e\xcc\x81\xcc\xa3"; + /* "e" + COMBINING DOT BELOW (U+0323) + COMBINING ACUTE ACCENT (U+0301) */ + static const char canonical_order[] = "e\xcc\xa3\xcc\x81"; + /* Two class-230 marks: GRAVE (U+0300) then ACUTE (U+0301) */ + static const char same_class[] = "e\xcc\x80\xcc\x81"; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + mock_sb->nls.char2uni = test_char2uni_utf8; + + /* Typed out of canonical order: must be reordered on storage. */ + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + wrong_order, strlen(wrong_order), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 3, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'e', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0323, be16_to_cpu(ustr.unicode[1])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[2])); + + /* Already in canonical order: must come out unchanged. */ + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + canonical_order, strlen(canonical_order), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 3, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'e', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0323, be16_to_cpu(ustr.unicode[1])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[2])); + + /* Two marks of equal combining class must keep their relative + * (input) order - the sort must be stable, not just "sorted". + */ + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + same_class, strlen(same_class), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 3, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'e', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0300, be16_to_cpu(ustr.unicode[1])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[2])); + + free_mock_sb(mock_sb); +} + +/* + * Test that a name written with combining marks in non-canonical order + * still reads back as valid, correct UTF-8 once reordered - i.e. that + * hfsplus_asc2uni() and hfsplus_uni2asc_str() stay consistent with each + * other across the reordering. + * + * NODECOMPOSE is set so hfsplus_uni2asc_str() doesn't also recompose + * "e" + COMBINING DOT BELOW back into the precomposed U+1EB9 ("e"): that + * composition behavior is real (and already covered elsewhere), but it + * would obscure what this test is actually checking. + */ +static void hfsplus_unicode_combining_reorder_roundtrip_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + static const char wrong_order[] = "e\xcc\x81\xcc\xa3"; + static const char expected[] = "e\xcc\xa3\xcc\x81"; /* canonical order */ + char buf[32]; + int len = sizeof(buf); + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + set_bit(HFSPLUS_SB_NODECOMPOSE, &mock_sb->sb_info.flags); + mock_sb->nls.char2uni = test_char2uni_utf8; + mock_sb->nls.uni2char = test_uni2char_utf8; + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + wrong_order, strlen(wrong_order), + HFS_REGULAR_NAME); + KUNIT_EXPECT_EQ(test, 0, result); + + result = hfsplus_uni2asc_str(&mock_sb->sb, &ustr, buf, &len); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, (int)strlen(expected), len); + KUNIT_EXPECT_MEMEQ(test, expected, buf, len); + + free_mock_sb(mock_sb); +} + +/* + * Test that hfsplus_asc2uni() decomposes a character using the corrected + * (post-2002/"Jaguar") canonical decomposition even when Apple Technote + * #1150's own decomposition table doesn't have an entry for it. + * + * U+01F8 (LATIN CAPITAL LETTER N WITH GRAVE) is exactly one of the + * characters macOS's own fsck_hfs (FixDecomps() in CatalogCheck.c) has + * flagged "Illegal name" for since 2002 when found stored undecomposed. + */ +static void hfsplus_asc2uni_legacy_decomp_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + static const char input[] = "\xc7\xb8"; /* U+01F8 */ + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + mock_sb->nls.char2uni = test_char2uni_utf8; + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + input, strlen(input), HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 2, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'N', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0300, be16_to_cpu(ustr.unicode[1])); + + free_mock_sb(mock_sb); +} + +/* + * Test that hfsplus_asc2uni() corrects two more of fsck_hfs's known + * legacy decomposition sequences once combining marks from independently + * typed characters end up adjacent: + * + * - GREEK SMALL LETTER ALPHA (U+03B1) + COMBINING VERTICAL LINE ABOVE + * (U+030D) must become U+03B1 + COMBINING ACUTE ACCENT (U+0301). + * - BENGALI LETTER BA (U+09AC) + BENGALI SIGN NUKTA (U+09BC) must become + * the single character BENGALI LETTER RA WITH MIDDLE DIAGONAL (U+09B0). + */ +static void hfsplus_asc2uni_legacy_seq_fixup_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + static const char greek_input[] = "\xce\xb1\xcc\x8d"; /* U+03B1 U+030D */ + static const char bengali_input[] = "\xe0\xa6\xac\xe0\xa6\xbc"; /* U+09AC U+09BC */ + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + mock_sb->nls.char2uni = test_char2uni_utf8; + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + greek_input, strlen(greek_input), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 2, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 0x03b1, be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[1])); + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + bengali_input, strlen(bengali_input), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 1, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 0x09b0, be16_to_cpu(ustr.unicode[0])); + + free_mock_sb(mock_sb); +} + /* Mock dentry for testing hfsplus_hash_dentry */ static struct dentry test_dentry; @@ -1231,6 +1448,37 @@ static void hfsplus_hash_dentry_edge_cases_test(struct kunit *test) free_mock_sb(mock_sb); } +/* + * Test that hfsplus_hash_dentry() hashes two names identically when they + * differ only in the (non-canonical) typed order of the same combining + * marks - both must canonicalize to the same stored form, so they must + * hash the same or dcache lookups would spuriously miss. + */ +static void hfsplus_hash_dentry_combining_reorder_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct qstr str1, str2; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + setup_mock_dentry(&mock_sb->sb); + mock_sb->nls.char2uni = test_char2uni_utf8; + + create_qstr(&str1, "e\xcc\x81\xcc\xa3"); /* acute, then dot-below */ + result = hfsplus_hash_dentry(&test_dentry, &str1); + KUNIT_EXPECT_EQ(test, 0, result); + + create_qstr(&str2, "e\xcc\xa3\xcc\x81"); /* dot-below, then acute */ + result = hfsplus_hash_dentry(&test_dentry, &str2); + KUNIT_EXPECT_EQ(test, 0, result); + + KUNIT_EXPECT_EQ(test, str1.hash, str2.hash); + + free_mock_sb(mock_sb); +} + /* Test hfsplus_compare_dentry basic functionality */ static void hfsplus_compare_dentry_basic_test(struct kunit *test) { @@ -1553,6 +1801,59 @@ static void hfsplus_compare_dentry_combined_flags_test(struct kunit *test) free_mock_sb(mock_sb); } +/* + * Test that hfsplus_compare_dentry() treats two names as equal when they + * differ only in the (non-canonical) typed order of the same combining + * marks, since both refer to the same canonically-ordered catalog entry. + */ +static void hfsplus_compare_dentry_combining_reorder_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct qstr name; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + setup_mock_dentry(&mock_sb->sb); + mock_sb->nls.char2uni = test_char2uni_utf8; + + create_qstr(&name, "e\xcc\xa3\xcc\x81"); /* dot-below, then acute */ + result = hfsplus_compare_dentry(&test_dentry, 5, "e\xcc\x81\xcc\xa3", + &name); /* acute, then dot-below */ + KUNIT_EXPECT_EQ(test, 0, result); + + free_mock_sb(mock_sb); +} + +/* + * Test that a precomposed character using one of fsck_hfs's known legacy + * decompositions compares equal to the already-decomposed form of the + * same character - i.e. that hfsplus_compare_dentry() applies the same + * legacy-decomposition correction as hfsplus_asc2uni() does on storage, + * so a lookup finds the entry regardless of which form was typed. + */ +static void hfsplus_compare_dentry_legacy_decomp_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct qstr name; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + setup_mock_dentry(&mock_sb->sb); + mock_sb->nls.char2uni = test_char2uni_utf8; + + /* "N" + COMBINING GRAVE ACCENT (U+0300), already decomposed */ + create_qstr(&name, "N\xcc\x80"); + /* U+01F8, precomposed */ + result = hfsplus_compare_dentry(&test_dentry, 2, "\xc7\xb8", &name); + KUNIT_EXPECT_EQ(test, 0, result); + + free_mock_sb(mock_sb); +} + static struct kunit_case hfsplus_unicode_test_cases[] = { KUNIT_CASE(hfsplus_strcasecmp_test), KUNIT_CASE(hfsplus_strcmp_test), @@ -1568,12 +1869,17 @@ static struct kunit_case hfsplus_unicode_test_cases[] = { KUNIT_CASE(hfsplus_asc2uni_buffer_limits_test), KUNIT_CASE(hfsplus_asc2uni_edge_cases_test), KUNIT_CASE(hfsplus_asc2uni_decompose_test), + KUNIT_CASE(hfsplus_asc2uni_combining_reorder_test), + KUNIT_CASE(hfsplus_unicode_combining_reorder_roundtrip_test), + KUNIT_CASE(hfsplus_asc2uni_legacy_decomp_test), + KUNIT_CASE(hfsplus_asc2uni_legacy_seq_fixup_test), KUNIT_CASE(hfsplus_hash_dentry_basic_test), KUNIT_CASE(hfsplus_hash_dentry_casefold_test), KUNIT_CASE(hfsplus_hash_dentry_special_chars_test), KUNIT_CASE(hfsplus_hash_dentry_decompose_test), KUNIT_CASE(hfsplus_hash_dentry_consistency_test), KUNIT_CASE(hfsplus_hash_dentry_edge_cases_test), + KUNIT_CASE(hfsplus_hash_dentry_combining_reorder_test), KUNIT_CASE(hfsplus_compare_dentry_basic_test), KUNIT_CASE(hfsplus_compare_dentry_casefold_test), KUNIT_CASE(hfsplus_compare_dentry_special_chars_test), @@ -1581,6 +1887,8 @@ static struct kunit_case hfsplus_unicode_test_cases[] = { KUNIT_CASE(hfsplus_compare_dentry_decompose_test), KUNIT_CASE(hfsplus_compare_dentry_edge_cases_test), KUNIT_CASE(hfsplus_compare_dentry_combined_flags_test), + KUNIT_CASE(hfsplus_compare_dentry_combining_reorder_test), + KUNIT_CASE(hfsplus_compare_dentry_legacy_decomp_test), {} }; -- 2.43.0