mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] hfsplus: make hidden directory name independent from nls
@ 2026-11-04  3:46 Viacheslav Dubeyko
  2026-11-04  3:46 ` [PATCH 1/2] hfsplus: make hidden directory name independent from mount's nls Viacheslav Dubeyko
  2026-11-04  3:46 ` [PATCH 2/2] hfsplus: add KUnit check for hidden directory name Viacheslav Dubeyko
  0 siblings, 2 replies; 3+ messages in thread
From: Viacheslav Dubeyko @ 2026-11-04  3:46 UTC (permalink / raw)
  To: glaubitz, frank.li
  Cc: linux-fsdevel, linux-kernel, vdubeyko, Viacheslav Dubeyko

HFS+ keeps the indirect node files of hard links in the hidden
directory of the root folder. On disk, its name is four U+0000 (NUL)
characters followed by "HFS+ Private Data". Currently, the catalog key
of this directory is built from the UTF-8 string HFSP_HIDDENDIR_NAME
by means of the nls table of the mount. hfsplus_fill_super() works
around it by temporarily replacing sbi->nls with utf8 table, but
hfsplus_reconfigure() creates the hidden directory on remount
read-write with the mount's nls table. As a result, with nls=cp437,
for example, the hidden directory is created with garbled name that
neither macOS nor the next mount recognizes.

At first, define the on-disk name of the hidden directory as
constant UTF-16 string hfsplus_hidden_dir_name_utf16, introduces
hfsplus_create_cat_uni() that creates catalog records for already
converted name, and removes the temporary nls replacement from
hfsplus_fill_super(). The length of the name is calculated from named
constants, and the UTF-8 and UTF-16 representations of the name are
distinguished by the HFSP_HIDDENDIR_NAME_UTF8* and
HFSP_HIDDENDIR_NAME_UTF16_LEN macros.

Secondly, add KUnit test that checks that hfsplus_asc2uni()
converts HFSP_HIDDENDIR_NAME_UTF8 with utf8 nls table into exactly
hfsplus_hidden_dir_name_utf16, with decomposition enabled and
disabled.

The hfsplus_unicode KUnit suite passes (35 tests) under UML.

Viacheslav Dubeyko (2):
  hfsplus: make hidden directory name independent from mount's nls
  hfsplus: add KUnit check for hidden directory name

 fs/hfsplus/catalog.c       | 83 ++++++++++++++++++++++++++++++--------
 fs/hfsplus/hfsplus_fs.h    |  6 +++
 fs/hfsplus/super.c         | 68 +++++++++++++++++--------------
 fs/hfsplus/unicode_test.c  | 55 +++++++++++++++++++++++++
 include/linux/hfs_common.h | 20 ++++++++-
 5 files changed, 182 insertions(+), 50 deletions(-)


base-commit: 71d5a8a05c83b61bce1884905c192eed34ed3069
-- 
2.43.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH 1/2] hfsplus: make hidden directory name independent from mount's nls
  2026-11-04  3:46 [PATCH 0/2] hfsplus: make hidden directory name independent from nls Viacheslav Dubeyko
@ 2026-11-04  3:46 ` Viacheslav Dubeyko
  2026-11-04  3:46 ` [PATCH 2/2] hfsplus: add KUnit check for hidden directory name Viacheslav Dubeyko
  1 sibling, 0 replies; 3+ messages in thread
From: Viacheslav Dubeyko @ 2026-11-04  3:46 UTC (permalink / raw)
  To: glaubitz, frank.li
  Cc: linux-fsdevel, linux-kernel, vdubeyko, Viacheslav Dubeyko

The catalog key of the hidden directory is built from
HFSP_HIDDENDIR_NAME, that is UTF-8 string, by means of the nls table
of the mount. hfsplus_fill_super() works around it by temporarily
replacing sbi->nls with utf8 table. However, hfsplus_reconfigure()
creates the hidden directory on remount read-write with the mount's
nls table. As a result, for example, with nls=cp437 the hidden
directory is created with garbled name that neither macOS nor
the next mount recognizes, and yet another hidden directory is
created on the next read-write mount. Temporary replacement of
sbi->nls is not an option in hfsplus_reconfigure() because lookups
are running concurrently and use sbi->nls.

Define the hidden directory name as constant Unicode string and
introduce hfsplus_create_cat_uni() that creates catalog records for
already converted name. Use hfsplus_cat_build_key_uni() for hidden
directory lookup and remove the temporary nls replacement from
hfsplus_fill_super(). The rollback in hfsplus_create_hidden_dir()
calls hfsplus_delete_cat() with NULL name, so the name is taken
from the thread record.

Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>
cc: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>
cc: Yangtao Li <frank.li@vivo.com>
cc: linux-fsdevel@vger.kernel.org
---
 fs/hfsplus/catalog.c       | 83 ++++++++++++++++++++++++++++++--------
 fs/hfsplus/hfsplus_fs.h    |  5 +++
 fs/hfsplus/super.c         | 66 ++++++++++++++++--------------
 include/linux/hfs_common.h | 20 ++++++++-
 4 files changed, 124 insertions(+), 50 deletions(-)

diff --git a/fs/hfsplus/catalog.c b/fs/hfsplus/catalog.c
index fe11c4b6dd99..5e53a978d420 100644
--- a/fs/hfsplus/catalog.c
+++ b/fs/hfsplus/catalog.c
@@ -40,8 +40,8 @@ int hfsplus_cat_bin_cmp_key(const hfsplus_btree_key *k1,
 }
 
 /* Generates key for catalog file/folders record. */
-int hfsplus_cat_build_key(struct super_block *sb,
-		hfsplus_btree_key *key, u32 parent, const struct qstr *str)
+int hfsplus_cat_build_key(struct super_block *sb, hfsplus_btree_key *key,
+			  u32 parent, const struct qstr *str)
 {
 	int len, err;
 
@@ -58,15 +58,15 @@ int hfsplus_cat_build_key(struct super_block *sb,
 
 /* Generates key for catalog thread record. */
 void hfsplus_cat_build_key_with_cnid(struct super_block *sb,
-			hfsplus_btree_key *key, u32 parent)
+				     hfsplus_btree_key *key, u32 parent)
 {
 	key->cat.parent = cpu_to_be32(parent);
 	key->cat.name.length = 0;
 	key->key_len = cpu_to_be16(6);
 }
 
-static void hfsplus_cat_build_key_uni(hfsplus_btree_key *key, u32 parent,
-				      struct hfsplus_unistr *name)
+void hfsplus_cat_build_key_uni(hfsplus_btree_key *key, u32 parent,
+			       const struct hfsplus_unistr *name)
 {
 	int ustrlen;
 
@@ -103,7 +103,7 @@ void hfsplus_cat_set_perms(struct inode *inode, struct hfsplus_perm *perms)
 }
 
 static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
-		u32 cnid, struct inode *inode)
+				    u32 cnid, struct inode *inode)
 {
 	struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb);
 
@@ -187,7 +187,23 @@ static int hfsplus_fill_cat_thread(struct super_block *sb,
 	if (unlikely(err < 0))
 		return err;
 
-	return 10 + be16_to_cpu(entry->thread.nodeName.length) * 2;
+	return hfsplus_cat_thread_size(&entry->thread);
+}
+
+static int hfsplus_fill_cat_thread_uni(hfsplus_cat_entry *entry, int type,
+				       u32 parentid,
+				       const struct hfsplus_unistr *name)
+{
+	u16 len = be16_to_cpu(name->length);
+
+	entry->type = cpu_to_be16(type);
+	entry->thread.reserved = 0;
+	entry->thread.parentID = cpu_to_be32(parentid);
+	entry->thread.nodeName.length = name->length;
+	memcpy(entry->thread.nodeName.unicode, name->unicode,
+	       len * sizeof(hfsplus_unichr));
+
+	return hfsplus_cat_thread_size(&entry->thread);
 }
 
 /* Try to get a catalog entry for given catalog id */
@@ -250,17 +266,25 @@ static void hfsplus_subfolders_dec(struct inode *dir)
 	}
 }
 
-int hfsplus_create_cat(u32 cnid, struct inode *dir,
-		const struct qstr *str, struct inode *inode)
+/*
+ * Create catalog records for the name given either as @str (it is converted
+ * by means of the mount's nls table) or as already converted @name.
+ */
+static int __hfsplus_create_cat(u32 cnid, struct inode *dir,
+				const struct qstr *str,
+				const struct hfsplus_unistr *name,
+				struct inode *inode)
 {
 	struct super_block *sb = dir->i_sb;
 	struct hfs_find_data fd;
 	hfsplus_cat_entry entry;
+	int type;
 	int entry_size;
 	int err;
 
 	hfs_dbg("name %s, cnid %u, i_nlink %d\n",
-		str->name, cnid, inode->i_nlink);
+		str ? (const char *)str->name : "<unicode>",
+		cnid, inode->i_nlink);
 	err = hfs_find_init(HFSPLUS_SB(sb)->cat_tree, &fd);
 	if (err)
 		return err;
@@ -274,10 +298,14 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 		goto err2;
 
 	hfsplus_cat_build_key_with_cnid(sb, fd.search_key, cnid);
-	entry_size = hfsplus_fill_cat_thread(sb, &entry,
-		S_ISDIR(inode->i_mode) ?
-			HFSPLUS_FOLDER_THREAD : HFSPLUS_FILE_THREAD,
-		dir->i_ino, str);
+	type = S_ISDIR(inode->i_mode) ?
+			HFSPLUS_FOLDER_THREAD : HFSPLUS_FILE_THREAD;
+	if (name)
+		entry_size = hfsplus_fill_cat_thread_uni(&entry, type,
+							 dir->i_ino, name);
+	else
+		entry_size = hfsplus_fill_cat_thread(sb, &entry, type,
+						     dir->i_ino, str);
 	if (unlikely(entry_size < 0)) {
 		err = entry_size;
 		goto err2;
@@ -293,9 +321,13 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 	if (err)
 		goto err2;
 
-	err = hfsplus_cat_build_key(sb, fd.search_key, dir->i_ino, str);
-	if (unlikely(err))
-		goto err1;
+	if (name)
+		hfsplus_cat_build_key_uni(fd.search_key, dir->i_ino, name);
+	else {
+		err = hfsplus_cat_build_key(sb, fd.search_key, dir->i_ino, str);
+		if (unlikely(err))
+			goto err1;
+	}
 
 	entry_size = hfsplus_cat_build_record(&entry, cnid, inode);
 	err = hfs_brec_find(&fd, hfs_find_rec_by_key);
@@ -328,6 +360,23 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 	return err;
 }
 
+int hfsplus_create_cat(u32 cnid, struct inode *dir,
+			const struct qstr *str, struct inode *inode)
+{
+	return __hfsplus_create_cat(cnid, dir, str, NULL, inode);
+}
+
+/*
+ * Create catalog records for the name that is independent from
+ * the mount's nls table (for example, the hidden directory name).
+ */
+int hfsplus_create_cat_uni(u32 cnid, struct inode *dir,
+			   const struct hfsplus_unistr *name,
+			   struct inode *inode)
+{
+	return __hfsplus_create_cat(cnid, dir, NULL, name, inode);
+}
+
 int hfsplus_delete_cat(u32 cnid, struct inode *dir, const struct qstr *str)
 {
 	struct super_block *sb = dir->i_sb;
diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h
index 6f240f87f8ff..e2d3f488142b 100644
--- a/fs/hfsplus/hfsplus_fs.h
+++ b/fs/hfsplus/hfsplus_fs.h
@@ -418,11 +418,16 @@ int hfsplus_cat_build_key(struct super_block *sb, hfsplus_btree_key *key,
 			   u32 parent, const struct qstr *str);
 void hfsplus_cat_build_key_with_cnid(struct super_block *sb,
 				     hfsplus_btree_key *key, u32 parent);
+void hfsplus_cat_build_key_uni(hfsplus_btree_key *key, u32 parent,
+			       const struct hfsplus_unistr *name);
 void hfsplus_cat_set_perms(struct inode *inode, struct hfsplus_perm *perms);
 int hfsplus_find_cat(struct super_block *sb, u32 cnid,
 		     struct hfs_find_data *fd);
 int hfsplus_create_cat(u32 cnid, struct inode *dir, const struct qstr *str,
 		       struct inode *inode);
+int hfsplus_create_cat_uni(u32 cnid, struct inode *dir,
+			   const struct hfsplus_unistr *name,
+			   struct inode *inode);
 int hfsplus_delete_cat(u32 cnid, struct inode *dir, const struct qstr *str);
 int hfsplus_rename_cat(u32 cnid, struct inode *src_dir, const struct qstr *src_name,
 		       struct inode *dst_dir, const struct qstr *dst_name);
diff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c
index 8c65741f2cf2..57858d8685ff 100644
--- a/fs/hfsplus/super.c
+++ b/fs/hfsplus/super.c
@@ -399,6 +399,28 @@ static int hfsplus_statfs(struct dentry *dentry, struct kstatfs *buf)
 	return 0;
 }
 
+/*
+ * On-disk UTF-16 name of the hidden directory: HFSP_HIDDENDIR_NUL_COUNT
+ * U+0000 (NUL) followed by HFSP_HIDDENDIR_SUFFIX. HFSP_HIDDENDIR_NAME_UTF8
+ * is the Linux representation of this name, where every NUL is replaced
+ * 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 = {
+	.length = cpu_to_be16(HFSP_HIDDENDIR_NAME_UTF16_LEN),
+	.unicode = {
+		/* unicode[0 .. HFSP_HIDDENDIR_NUL_COUNT - 1] are U+0000 */
+		[HFSP_HIDDENDIR_NUL_COUNT] =
+		cpu_to_be16('H'), cpu_to_be16('F'), cpu_to_be16('S'),
+		cpu_to_be16('+'), cpu_to_be16(' '),
+		cpu_to_be16('P'), cpu_to_be16('r'), cpu_to_be16('i'),
+		cpu_to_be16('v'), cpu_to_be16('a'), cpu_to_be16('t'),
+		cpu_to_be16('e'), cpu_to_be16(' '),
+		cpu_to_be16('D'), cpu_to_be16('a'), cpu_to_be16('t'),
+		cpu_to_be16('a'),
+	},
+};
+
 /*
  * Create the hidden directory that keeps the indirect node files
  * of hard links. The caller is responsible for cancelling the
@@ -406,11 +428,12 @@ static int hfsplus_statfs(struct dentry *dentry, struct kstatfs *buf)
  * set to true if the catalog file could be left in inconsistent state.
  */
 static int hfsplus_create_hidden_dir(struct super_block *sb,
-				     const struct qstr *str,
 				     bool *cat_damaged)
 {
 	struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
 	struct inode *root = d_inode(sb->s_root);
+	const struct qstr str = QSTR_INIT(HFSP_HIDDENDIR_NAME_UTF8,
+					  HFSP_HIDDENDIR_NAME_UTF8_LEN);
 	struct inode *inode;
 	int err;
 
@@ -423,15 +446,17 @@ static int hfsplus_create_hidden_dir(struct super_block *sb,
 		goto out_unlock;
 	}
 
-	err = hfsplus_create_cat(inode->i_ino, root, str, inode);
+	err = hfsplus_create_cat_uni(inode->i_ino, root,
+				     &hfsplus_hidden_dir_name_utf16, inode);
 	if (err)
 		goto out_put_inode;
 
-	err = hfsplus_init_security(inode, root, str);
+	err = hfsplus_init_security(inode, root, &str);
 	if (err == -EOPNOTSUPP)
 		err = 0; /* Operation is not supported. */
 	else if (err) {
-		if (hfsplus_delete_cat(inode->i_ino, root, str)) {
+		/* The name is taken from the thread record. */
+		if (hfsplus_delete_cat(inode->i_ino, root, NULL)) {
 			pr_err("unable to remove hidden directory record\n");
 			*cat_damaged = true;
 		}
@@ -520,11 +545,9 @@ static int hfsplus_reconfigure(struct fs_context *fc)
 	 * It could be absent if the volume was mounted read-only.
 	 */
 	if (!sbi->hidden_dir) {
-		struct qstr str = QSTR_INIT(HFSP_HIDDENDIR_NAME,
-					    sizeof(HFSP_HIDDENDIR_NAME) - 1);
 		bool cat_damaged;
 
-		err = hfsplus_create_hidden_dir(sb, &str, &cat_damaged);
+		err = hfsplus_create_hidden_dir(sb, &cat_damaged);
 		if (err) {
 			pr_err("unable to create hidden directory: err %d\n",
 			       err);
@@ -562,7 +585,6 @@ void hfsplus_prepare_volume_header_for_commit(struct hfsplus_vh *vhdr)
 }
 
 static inline int hfsplus_get_hidden_dir_entry(struct super_block *sb,
-					       const struct qstr *str,
 					       hfsplus_cat_entry *entry)
 {
 	struct hfs_find_data fd;
@@ -572,15 +594,13 @@ static inline int hfsplus_get_hidden_dir_entry(struct super_block *sb,
 	if (unlikely(err))
 		return err;
 
-	err = hfsplus_cat_build_key(sb, fd.search_key, HFSPLUS_ROOT_CNID, str);
-	if (unlikely(err))
-		goto free_fd;
+	hfsplus_cat_build_key_uni(fd.search_key, HFSPLUS_ROOT_CNID,
+				  &hfsplus_hidden_dir_name_utf16);
 
 	err = hfsplus_brec_read_cat(&fd, entry);
 	if (err)
 		err = -ENOENT;
 
-free_fd:
 	hfs_find_exit(&fd);
 	return err;
 }
@@ -591,8 +611,6 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)
 	struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
 	hfsplus_cat_entry entry;
 	struct inode *root, *inode;
-	struct qstr str;
-	struct nls_table *nls;
 	u64 last_fs_block, last_fs_page;
 	int silent = fc->sb_flags & SB_SILENT;
 	int err;
@@ -610,19 +628,11 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)
 			sbi->nls = load_nls_default();
 	}
 
-	/* temporarily use utf8 to correctly find the hidden dir below */
-	nls = sbi->nls;
-	sbi->nls = load_nls("utf8");
-	if (!sbi->nls) {
-		pr_err("unable to load nls for utf8\n");
-		goto out_unload_nls;
-	}
-
 	/* Grab the volume header */
 	if (hfsplus_read_wrapper(sb)) {
 		if (!silent)
 			pr_warn("unable to find HFS+ superblock\n");
-		goto out_unload_nls;
+		return err;
 	}
 	vhdr = sbi->s_vhdr;
 
@@ -723,9 +733,7 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)
 		goto out_put_alloc_file;
 	}
 
-	str = (struct qstr)QSTR_INIT(HFSP_HIDDENDIR_NAME,
-				     sizeof(HFSP_HIDDENDIR_NAME) - 1);
-	err = hfsplus_get_hidden_dir_entry(sb, &str, &entry);
+	err = hfsplus_get_hidden_dir_entry(sb, &entry);
 	if (err == -ENOENT) {
 		/*
 		 * Hidden directory is absent or it cannot be read.
@@ -756,14 +764,12 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)
 		if (!sbi->hidden_dir) {
 			bool cat_damaged;
 
-			err = hfsplus_create_hidden_dir(sb, &str, &cat_damaged);
+			err = hfsplus_create_hidden_dir(sb, &cat_damaged);
 			if (err)
 				goto out_cancel_sync_work;
 		}
 	}
 
-	unload_nls(sbi->nls);
-	sbi->nls = nls;
 	return 0;
 
 out_cancel_sync_work:
@@ -782,8 +788,6 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)
 out_free_vhdr:
 	kfree(sbi->s_vhdr_buf);
 	kfree(sbi->s_backup_vhdr_buf);
-out_unload_nls:
-	unload_nls(nls);
 	return err;
 }
 
diff --git a/include/linux/hfs_common.h b/include/linux/hfs_common.h
index d6a615e74b26..8a7bc9763eb9 100644
--- a/include/linux/hfs_common.h
+++ b/include/linux/hfs_common.h
@@ -57,8 +57,24 @@
 
 #define HFSP_MOUNT_VERSION		0x482b4c78	/* 'H+Lx' */
 
-#define HFSP_HIDDENDIR_NAME \
-	"\xe2\x90\x80\xe2\x90\x80\xe2\x90\x80\xe2\x90\x80HFS+ Private Data"
+/*
+ * The name of the hidden directory is HFSP_HIDDENDIR_NUL_COUNT U+0000
+ * characters followed by HFSP_HIDDENDIR_SUFFIX. On disk, it is stored
+ * in UTF-16 and has HFSP_HIDDENDIR_NAME_UTF16_LEN characters. In Linux,
+ * it is HFSP_HIDDENDIR_NAME_UTF8, where every U+0000 is represented by
+ * U+2400 (SYMBOL FOR NULL); it has HFSP_HIDDENDIR_NAME_UTF8_LEN bytes.
+ */
+#define HFSP_HIDDENDIR_NUL_COUNT	4
+#define HFSP_HIDDENDIR_SUFFIX		"HFS+ Private Data"
+#define HFSP_HIDDENDIR_SUFFIX_LEN	(sizeof(HFSP_HIDDENDIR_SUFFIX) - 1)
+#define HFSP_HIDDENDIR_NAME_UTF16_LEN \
+	(HFSP_HIDDENDIR_NUL_COUNT + HFSP_HIDDENDIR_SUFFIX_LEN)
+#define HFSP_SYMBOL_FOR_NULL_UTF8	"\xe2\x90\x80"
+#define HFSP_HIDDENDIR_NAME_UTF8 \
+	(HFSP_SYMBOL_FOR_NULL_UTF8 HFSP_SYMBOL_FOR_NULL_UTF8 \
+	 HFSP_SYMBOL_FOR_NULL_UTF8 HFSP_SYMBOL_FOR_NULL_UTF8 \
+	 HFSP_HIDDENDIR_SUFFIX)
+#define HFSP_HIDDENDIR_NAME_UTF8_LEN	(sizeof(HFSP_HIDDENDIR_NAME_UTF8) - 1)
 
 /* various FIXED size parameters */
 #define HFS_SECTOR_SIZE			512	/* size of an HFS sector */
-- 
2.43.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH 2/2] hfsplus: add KUnit check for hidden directory name
  2026-11-04  3:46 [PATCH 0/2] hfsplus: make hidden directory name independent from nls Viacheslav Dubeyko
  2026-11-04  3:46 ` [PATCH 1/2] hfsplus: make hidden directory name independent from mount's nls Viacheslav Dubeyko
@ 2026-11-04  3:46 ` Viacheslav Dubeyko
  1 sibling, 0 replies; 3+ messages in thread
From: Viacheslav Dubeyko @ 2026-11-04  3:46 UTC (permalink / raw)
  To: glaubitz, frank.li
  Cc: linux-fsdevel, linux-kernel, vdubeyko, Viacheslav Dubeyko

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@dubeyko.com>
cc: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>
cc: Yangtao Li <frank.li@vivo.com>
cc: linux-fsdevel@vger.kernel.org
---
 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


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-11-04  3:46 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-04  3:46 [PATCH 0/2] hfsplus: make hidden directory name independent from nls Viacheslav Dubeyko
2026-11-04  3:46 ` [PATCH 1/2] hfsplus: make hidden directory name independent from mount's nls Viacheslav Dubeyko
2026-11-04  3:46 ` [PATCH 2/2] hfsplus: add KUnit check for hidden directory name Viacheslav Dubeyko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®