mirror of
https://github.com/torvalds/linux.git
synced 2026-10-23 23:41:02 +02:00
hfsplus: add KUnit coverage for canonical reordering and legacy fixups
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 <slava@dubeyko.com> Link: https://lore.kernel.org/r/20260919001607.2777138-4-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>
This commit is contained in:
parent
bbe5ebcbc0
commit
28d4963409
|
|
@ -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),
|
||||
{}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user