[PATCH 2/2] hfsplus: add KUnit check for hidden directory name
From: Viacheslav Dubeyko
Date: Tue Nov 03 2026 - 22:46:51 EST
The hidden directory is created and looked up by means of
precomputed UTF-16 name hfsplus_hidden_dir_name_utf16, while its
Linux representation HFSP_HIDDENDIR_NAME_UTF8 is still used
elsewhere. Add regression test that checks that hfsplus_asc2uni()
converts HFSP_HIDDENDIR_NAME_UTF8 with utf8 nls table into exactly
the same UTF-16 string, with decomposition enabled and disabled.
In particular, it checks that every U+2400 (SYMBOL FOR NULL) of
HFSP_HIDDENDIR_NAME_UTF8 is stored as U+0000 (NUL), as macOS
expects, and that the length of the converted name is
HFSP_HIDDENDIR_NAME_UTF16_LEN.
Signed-off-by: Viacheslav Dubeyko <slava@xxxxxxxxxxx>
cc: John Paul Adrian Glaubitz <glaubitz@xxxxxxxxxxxxxxxxxxx>
cc: Yangtao Li <frank.li@xxxxxxxx>
cc: linux-fsdevel@xxxxxxxxxxxxxxx
---
fs/hfsplus/hfsplus_fs.h | 1 +
fs/hfsplus/super.c | 4 ++-
fs/hfsplus/unicode_test.c | 55 +++++++++++++++++++++++++++++++++++++++
3 files changed, 59 insertions(+), 1 deletion(-)
diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h
index e2d3f488142b..dd88053cc9b3 100644
--- a/fs/hfsplus/hfsplus_fs.h
+++ b/fs/hfsplus/hfsplus_fs.h
@@ -487,6 +487,7 @@ int hfs_part_find(struct super_block *sb, sector_t *part_start,
sector_t *part_size);
/* super.c */
+extern const struct hfsplus_unistr hfsplus_hidden_dir_name_utf16;
struct inode *hfsplus_iget(struct super_block *sb, unsigned long ino);
void hfsplus_mark_mdb_dirty(struct super_block *sb);
void hfsplus_prepare_volume_header_for_commit(struct hfsplus_vh *vhdr);
diff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c
index 57858d8685ff..4e878641fb59 100644
--- a/fs/hfsplus/super.c
+++ b/fs/hfsplus/super.c
@@ -18,6 +18,7 @@
#include <linux/slab.h>
#include <linux/vfs.h>
#include <linux/nls.h>
+#include <kunit/visibility.h>
static struct inode *hfsplus_alloc_inode(struct super_block *sb);
static void hfsplus_free_inode(struct inode *inode);
@@ -406,7 +407,7 @@ static int hfsplus_statfs(struct dentry *dentry, struct kstatfs *buf)
* by U+2400 (SYMBOL FOR NULL). The catalog key of the hidden directory
* must not depend on the nls table of the mount.
*/
-static const struct hfsplus_unistr hfsplus_hidden_dir_name_utf16 = {
+const struct hfsplus_unistr hfsplus_hidden_dir_name_utf16 = {
.length = cpu_to_be16(HFSP_HIDDENDIR_NAME_UTF16_LEN),
.unicode = {
/* unicode[0 .. HFSP_HIDDENDIR_NUL_COUNT - 1] are U+0000 */
@@ -420,6 +421,7 @@ static const struct hfsplus_unistr hfsplus_hidden_dir_name_utf16 = {
cpu_to_be16('a'),
},
};
+EXPORT_SYMBOL_IF_KUNIT(hfsplus_hidden_dir_name_utf16);
/*
* Create the hidden directory that keeps the indirect node files
diff --git a/fs/hfsplus/unicode_test.c b/fs/hfsplus/unicode_test.c
index a683c6d5fc22..cb237a40c0c3 100644
--- a/fs/hfsplus/unicode_test.c
+++ b/fs/hfsplus/unicode_test.c
@@ -1165,6 +1165,60 @@ static void hfsplus_asc2uni_legacy_seq_fixup_test(struct kunit *test)
free_mock_sb(mock_sb);
}
+/*
+ * Test that the precomputed Unicode name of the hidden directory is
+ * exactly what hfsplus_asc2uni() produces from HFSP_HIDDENDIR_NAME_UTF8
+ * with utf8 nls table. Otherwise, the catalog key of the hidden
+ * directory created or looked up by the driver will not match the key
+ * that macOS (and older versions of the driver) uses for this folder.
+ * The result must not depend on the decomposition setting either.
+ */
+static void hfsplus_hidden_dir_name_test(struct kunit *test)
+{
+ struct test_mock_sb *mock_sb;
+ struct hfsplus_unistr ustr;
+ const struct hfsplus_unistr *expected = &hfsplus_hidden_dir_name_utf16;
+ u16 len = be16_to_cpu(expected->length);
+ int result;
+
+ mock_sb = setup_mock_sb();
+ KUNIT_ASSERT_NOT_NULL(test, mock_sb);
+
+ mock_sb->nls.char2uni = test_char2uni_utf8;
+
+ KUNIT_EXPECT_EQ(test, HFSP_HIDDENDIR_NAME_UTF16_LEN, len);
+
+ /* Decomposition is enabled by default */
+ memset(&ustr, 0, sizeof(ustr));
+ result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN,
+ HFSP_HIDDENDIR_NAME_UTF8,
+ HFSP_HIDDENDIR_NAME_UTF8_LEN,
+ HFS_REGULAR_NAME);
+
+ KUNIT_EXPECT_EQ(test, 0, result);
+ KUNIT_EXPECT_EQ(test, len, be16_to_cpu(ustr.length));
+ KUNIT_EXPECT_MEMEQ(test, expected->unicode, ustr.unicode,
+ len * sizeof(ustr.unicode[0]));
+ KUNIT_EXPECT_EQ(test, 0, hfsplus_strcmp(expected, &ustr));
+
+ /* The same name with disabled decomposition */
+ set_bit(HFSPLUS_SB_NODECOMPOSE, &mock_sb->sb_info.flags);
+
+ memset(&ustr, 0, sizeof(ustr));
+ result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN,
+ HFSP_HIDDENDIR_NAME_UTF8,
+ HFSP_HIDDENDIR_NAME_UTF8_LEN,
+ HFS_REGULAR_NAME);
+
+ KUNIT_EXPECT_EQ(test, 0, result);
+ KUNIT_EXPECT_EQ(test, len, be16_to_cpu(ustr.length));
+ KUNIT_EXPECT_MEMEQ(test, expected->unicode, ustr.unicode,
+ len * sizeof(ustr.unicode[0]));
+ KUNIT_EXPECT_EQ(test, 0, hfsplus_strcmp(expected, &ustr));
+
+ free_mock_sb(mock_sb);
+}
+
/* Mock dentry for testing hfsplus_hash_dentry */
static struct dentry test_dentry;
@@ -1873,6 +1927,7 @@ static struct kunit_case hfsplus_unicode_test_cases[] = {
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_hidden_dir_name_test),
KUNIT_CASE(hfsplus_hash_dentry_basic_test),
KUNIT_CASE(hfsplus_hash_dentry_casefold_test),
KUNIT_CASE(hfsplus_hash_dentry_special_chars_test),
--
2.43.0