diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c index 41b4e8fc9463..4f0ddc76e8c5 100644 --- a/fs/hfs/btree.c +++ b/fs/hfs/btree.c @@ -310,15 +310,18 @@ void hfs_btree_close(struct hfs_btree *tree) return; for (i = 0; i < NODE_HASH_SIZE; i++) { + spin_lock(&tree->hash_lock); while ((node = tree->node_hash[i])) { - tree->node_hash[i] = node->next_hash; + hfs_bnode_unhash(node); + spin_unlock(&tree->hash_lock); if (atomic_read(&node->refcnt)) pr_err("node %d:%d still has %d user(s)!\n", node->tree->cnid, node->this, atomic_read(&node->refcnt)); hfs_bnode_free(node); - tree->node_hash_cnt--; + spin_lock(&tree->hash_lock); } + spin_unlock(&tree->hash_lock); } iput(tree->inode); kfree(tree); diff --git a/fs/hfs/btree.h b/fs/hfs/btree.h index b4c3f2a31471..1f703f2c53d8 100644 --- a/fs/hfs/btree.h +++ b/fs/hfs/btree.h @@ -84,6 +84,25 @@ struct hfs_find_data { int entryoffset, entrylength; }; +static inline bool is_hfs_bnode_range_valid(struct hfs_find_data *fd, + size_t expected_len) +{ + struct hfs_bnode *node; + + if (!fd) + return false; + + node = fd->bnode; + if (!node || !node->tree) + return false; + + if (fd->entryoffset < 0 || fd->entrylength != expected_len) + return false; + + return (u64)fd->entryoffset + fd->entrylength <= + node->tree->node_size; +} + /* btree.c */ extern struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, diff --git a/fs/hfs/extent.c b/fs/hfs/extent.c index f066a99a863b..26357086517f 100644 --- a/fs/hfs/extent.c +++ b/fs/hfs/extent.c @@ -121,12 +121,18 @@ static int __hfs_ext_write_extent(struct inode *inode, struct hfs_find_data *fd) res = hfs_bmap_reserve(fd->tree, fd->tree->depth + 1); if (res) return res; - hfs_brec_insert(fd, HFS_I(inode)->cached_extents, sizeof(hfs_extent_rec)); + res = hfs_brec_insert(fd, HFS_I(inode)->cached_extents, + sizeof(hfs_extent_rec)); + if (res) + return res; HFS_I(inode)->flags &= ~(HFS_FLG_EXT_DIRTY|HFS_FLG_EXT_NEW); } else { if (res) return res; - hfs_bnode_write(fd->bnode, HFS_I(inode)->cached_extents, fd->entryoffset, fd->entrylength); + if (!is_hfs_bnode_range_valid(fd, sizeof(hfs_extent_rec))) + return -ERANGE; + hfs_bnode_write(fd->bnode, HFS_I(inode)->cached_extents, + fd->entryoffset, fd->entrylength); HFS_I(inode)->flags &= ~HFS_FLG_EXT_DIRTY; } return 0; diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index c81314f668ac..40c169d67664 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -663,7 +663,7 @@ int hfs_inode_setattr(const struct mnt_idmap *idmap, struct dentry *dentry, ((S_ISDIR(inode->i_mode) && (attr->ia_mode != inode->i_mode)) || (attr->ia_mode & ~HFS_VALID_MODE_BITS)))) { - return hsb->s_quiet ? 0 : error; + return hsb->s_quiet ? 0 : -EPERM; } /* map file permissions to the closest allowable permissions in HFS */ diff --git a/fs/hfsplus/btree.c b/fs/hfsplus/btree.c index 2ea8cd5658e1..bd4dbdbc8f98 100644 --- a/fs/hfsplus/btree.c +++ b/fs/hfsplus/btree.c @@ -417,16 +417,19 @@ void hfs_btree_close(struct hfs_btree *tree) return; for (i = 0; i < NODE_HASH_SIZE; i++) { + spin_lock(&tree->hash_lock); while ((node = tree->node_hash[i])) { - tree->node_hash[i] = node->next_hash; + hfs_bnode_unhash(node); + spin_unlock(&tree->hash_lock); if (atomic_read(&node->refcnt)) pr_crit("node %d:%d " "still has %d user(s)!\n", node->tree->cnid, node->this, atomic_read(&node->refcnt)); hfs_bnode_free(node); - tree->node_hash_cnt--; + spin_lock(&tree->hash_lock); } + spin_unlock(&tree->hash_lock); } iput(tree->inode); kfree(tree); diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h index d55cb16e3897..6f240f87f8ff 100644 --- a/fs/hfsplus/hfsplus_fs.h +++ b/fs/hfsplus/hfsplus_fs.h @@ -17,6 +17,7 @@ #include #include #include "hfsplus_raw.h" +#include "unicode.h" /* Runtime config options */ #define HFSPLUS_DEF_CR_TYPE 0x3F3F3F3F /* '????' */ @@ -486,11 +487,6 @@ void hfsplus_mark_mdb_dirty(struct super_block *sb); void hfsplus_prepare_volume_header_for_commit(struct hfsplus_vh *vhdr); int hfsplus_commit_superblock(struct super_block *sb); -/* tables.c */ -extern u16 hfsplus_case_fold_table[]; -extern u16 hfsplus_decompose_table[]; -extern u16 hfsplus_compose_table[]; - /* unicode.c */ int hfsplus_strcasecmp(const struct hfsplus_unistr *s1, const struct hfsplus_unistr *s2); diff --git a/fs/hfsplus/tables.c b/fs/hfsplus/tables.c index a5fb8ee7d019..2101201ab973 100644 --- a/fs/hfsplus/tables.c +++ b/fs/hfsplus/tables.c @@ -3244,3 +3244,499 @@ u16 hfsplus_compose_table[] = { /* 0x0342 0x0314 0x03c9 0x0345 */ 0x1fa7, 0x0000, }; + +/* + * Unicode canonical combining class (ccc) table, restricted to the Basic + * Multilingual Plane: HFS+ names are stored as UTF-16 and this kernel's + * wchar_t (see MAX_WCHAR_T in include/linux/nls.h) tops out at U+FFFF, so + * no HFS+ catalog name can contain a code point above the BMP anyway. + * + * Generated from the Unicode Character Database, version 12.1.0 + * (https://www.unicode.org/Public/12.1.0/ucd/extracted/DerivedCombiningClass.txt), + * the same UCD version fs/unicode/README.utf8data documents. Class 0 + * ("Not_Reordered") is the default for every code point not listed here + * and needs no entry. + * + * Used by hfsplus_canonical_reorder() to apply the Unicode Canonical + * Ordering Algorithm to a decomposed name, matching what macOS's own + * fsck_hfs (FixDecomps()) requires of a stored catalog name. + */ +struct hfsplus_ccc_range hfsplus_ccc_table[] = { + { 0x0300, 0x0314, 230 }, + { 0x0315, 0x0315, 232 }, + { 0x0316, 0x0319, 220 }, + { 0x031a, 0x031a, 232 }, + { 0x031b, 0x031b, 216 }, + { 0x031c, 0x0320, 220 }, + { 0x0321, 0x0322, 202 }, + { 0x0323, 0x0326, 220 }, + { 0x0327, 0x0328, 202 }, + { 0x0329, 0x0333, 220 }, + { 0x0334, 0x0338, 1 }, + { 0x0339, 0x033c, 220 }, + { 0x033d, 0x0344, 230 }, + { 0x0345, 0x0345, 240 }, + { 0x0346, 0x0346, 230 }, + { 0x0347, 0x0349, 220 }, + { 0x034a, 0x034c, 230 }, + { 0x034d, 0x034e, 220 }, + { 0x0350, 0x0352, 230 }, + { 0x0353, 0x0356, 220 }, + { 0x0357, 0x0357, 230 }, + { 0x0358, 0x0358, 232 }, + { 0x0359, 0x035a, 220 }, + { 0x035b, 0x035b, 230 }, + { 0x035c, 0x035c, 233 }, + { 0x035d, 0x035e, 234 }, + { 0x035f, 0x035f, 233 }, + { 0x0360, 0x0361, 234 }, + { 0x0362, 0x0362, 233 }, + { 0x0363, 0x036f, 230 }, + { 0x0483, 0x0487, 230 }, + { 0x0591, 0x0591, 220 }, + { 0x0592, 0x0595, 230 }, + { 0x0596, 0x0596, 220 }, + { 0x0597, 0x0599, 230 }, + { 0x059a, 0x059a, 222 }, + { 0x059b, 0x059b, 220 }, + { 0x059c, 0x05a1, 230 }, + { 0x05a2, 0x05a7, 220 }, + { 0x05a8, 0x05a9, 230 }, + { 0x05aa, 0x05aa, 220 }, + { 0x05ab, 0x05ac, 230 }, + { 0x05ad, 0x05ad, 222 }, + { 0x05ae, 0x05ae, 228 }, + { 0x05af, 0x05af, 230 }, + { 0x05b0, 0x05b0, 10 }, + { 0x05b1, 0x05b1, 11 }, + { 0x05b2, 0x05b2, 12 }, + { 0x05b3, 0x05b3, 13 }, + { 0x05b4, 0x05b4, 14 }, + { 0x05b5, 0x05b5, 15 }, + { 0x05b6, 0x05b6, 16 }, + { 0x05b7, 0x05b7, 17 }, + { 0x05b8, 0x05b8, 18 }, + { 0x05b9, 0x05ba, 19 }, + { 0x05bb, 0x05bb, 20 }, + { 0x05bc, 0x05bc, 21 }, + { 0x05bd, 0x05bd, 22 }, + { 0x05bf, 0x05bf, 23 }, + { 0x05c1, 0x05c1, 24 }, + { 0x05c2, 0x05c2, 25 }, + { 0x05c4, 0x05c4, 230 }, + { 0x05c5, 0x05c5, 220 }, + { 0x05c7, 0x05c7, 18 }, + { 0x0610, 0x0617, 230 }, + { 0x0618, 0x0618, 30 }, + { 0x0619, 0x0619, 31 }, + { 0x061a, 0x061a, 32 }, + { 0x064b, 0x064b, 27 }, + { 0x064c, 0x064c, 28 }, + { 0x064d, 0x064d, 29 }, + { 0x064e, 0x064e, 30 }, + { 0x064f, 0x064f, 31 }, + { 0x0650, 0x0650, 32 }, + { 0x0651, 0x0651, 33 }, + { 0x0652, 0x0652, 34 }, + { 0x0653, 0x0654, 230 }, + { 0x0655, 0x0656, 220 }, + { 0x0657, 0x065b, 230 }, + { 0x065c, 0x065c, 220 }, + { 0x065d, 0x065e, 230 }, + { 0x065f, 0x065f, 220 }, + { 0x0670, 0x0670, 35 }, + { 0x06d6, 0x06dc, 230 }, + { 0x06df, 0x06e2, 230 }, + { 0x06e3, 0x06e3, 220 }, + { 0x06e4, 0x06e4, 230 }, + { 0x06e7, 0x06e8, 230 }, + { 0x06ea, 0x06ea, 220 }, + { 0x06eb, 0x06ec, 230 }, + { 0x06ed, 0x06ed, 220 }, + { 0x0711, 0x0711, 36 }, + { 0x0730, 0x0730, 230 }, + { 0x0731, 0x0731, 220 }, + { 0x0732, 0x0733, 230 }, + { 0x0734, 0x0734, 220 }, + { 0x0735, 0x0736, 230 }, + { 0x0737, 0x0739, 220 }, + { 0x073a, 0x073a, 230 }, + { 0x073b, 0x073c, 220 }, + { 0x073d, 0x073d, 230 }, + { 0x073e, 0x073e, 220 }, + { 0x073f, 0x0741, 230 }, + { 0x0742, 0x0742, 220 }, + { 0x0743, 0x0743, 230 }, + { 0x0744, 0x0744, 220 }, + { 0x0745, 0x0745, 230 }, + { 0x0746, 0x0746, 220 }, + { 0x0747, 0x0747, 230 }, + { 0x0748, 0x0748, 220 }, + { 0x0749, 0x074a, 230 }, + { 0x07eb, 0x07f1, 230 }, + { 0x07f2, 0x07f2, 220 }, + { 0x07f3, 0x07f3, 230 }, + { 0x07fd, 0x07fd, 220 }, + { 0x0816, 0x0819, 230 }, + { 0x081b, 0x0823, 230 }, + { 0x0825, 0x0827, 230 }, + { 0x0829, 0x082d, 230 }, + { 0x0859, 0x085b, 220 }, + { 0x08d3, 0x08d3, 220 }, + { 0x08d4, 0x08e1, 230 }, + { 0x08e3, 0x08e3, 220 }, + { 0x08e4, 0x08e5, 230 }, + { 0x08e6, 0x08e6, 220 }, + { 0x08e7, 0x08e8, 230 }, + { 0x08e9, 0x08e9, 220 }, + { 0x08ea, 0x08ec, 230 }, + { 0x08ed, 0x08ef, 220 }, + { 0x08f0, 0x08f0, 27 }, + { 0x08f1, 0x08f1, 28 }, + { 0x08f2, 0x08f2, 29 }, + { 0x08f3, 0x08f5, 230 }, + { 0x08f6, 0x08f6, 220 }, + { 0x08f7, 0x08f8, 230 }, + { 0x08f9, 0x08fa, 220 }, + { 0x08fb, 0x08ff, 230 }, + { 0x093c, 0x093c, 7 }, + { 0x094d, 0x094d, 9 }, + { 0x0951, 0x0951, 230 }, + { 0x0952, 0x0952, 220 }, + { 0x0953, 0x0954, 230 }, + { 0x09bc, 0x09bc, 7 }, + { 0x09cd, 0x09cd, 9 }, + { 0x09fe, 0x09fe, 230 }, + { 0x0a3c, 0x0a3c, 7 }, + { 0x0a4d, 0x0a4d, 9 }, + { 0x0abc, 0x0abc, 7 }, + { 0x0acd, 0x0acd, 9 }, + { 0x0b3c, 0x0b3c, 7 }, + { 0x0b4d, 0x0b4d, 9 }, + { 0x0bcd, 0x0bcd, 9 }, + { 0x0c4d, 0x0c4d, 9 }, + { 0x0c55, 0x0c55, 84 }, + { 0x0c56, 0x0c56, 91 }, + { 0x0cbc, 0x0cbc, 7 }, + { 0x0ccd, 0x0ccd, 9 }, + { 0x0d3b, 0x0d3c, 9 }, + { 0x0d4d, 0x0d4d, 9 }, + { 0x0dca, 0x0dca, 9 }, + { 0x0e38, 0x0e39, 103 }, + { 0x0e3a, 0x0e3a, 9 }, + { 0x0e48, 0x0e4b, 107 }, + { 0x0eb8, 0x0eb9, 118 }, + { 0x0eba, 0x0eba, 9 }, + { 0x0ec8, 0x0ecb, 122 }, + { 0x0f18, 0x0f19, 220 }, + { 0x0f35, 0x0f35, 220 }, + { 0x0f37, 0x0f37, 220 }, + { 0x0f39, 0x0f39, 216 }, + { 0x0f71, 0x0f71, 129 }, + { 0x0f72, 0x0f72, 130 }, + { 0x0f74, 0x0f74, 132 }, + { 0x0f7a, 0x0f7d, 130 }, + { 0x0f80, 0x0f80, 130 }, + { 0x0f82, 0x0f83, 230 }, + { 0x0f84, 0x0f84, 9 }, + { 0x0f86, 0x0f87, 230 }, + { 0x0fc6, 0x0fc6, 220 }, + { 0x1037, 0x1037, 7 }, + { 0x1039, 0x103a, 9 }, + { 0x108d, 0x108d, 220 }, + { 0x135d, 0x135f, 230 }, + { 0x1714, 0x1714, 9 }, + { 0x1734, 0x1734, 9 }, + { 0x17d2, 0x17d2, 9 }, + { 0x17dd, 0x17dd, 230 }, + { 0x18a9, 0x18a9, 228 }, + { 0x1939, 0x1939, 222 }, + { 0x193a, 0x193a, 230 }, + { 0x193b, 0x193b, 220 }, + { 0x1a17, 0x1a17, 230 }, + { 0x1a18, 0x1a18, 220 }, + { 0x1a60, 0x1a60, 9 }, + { 0x1a75, 0x1a7c, 230 }, + { 0x1a7f, 0x1a7f, 220 }, + { 0x1ab0, 0x1ab4, 230 }, + { 0x1ab5, 0x1aba, 220 }, + { 0x1abb, 0x1abc, 230 }, + { 0x1abd, 0x1abd, 220 }, + { 0x1b34, 0x1b34, 7 }, + { 0x1b44, 0x1b44, 9 }, + { 0x1b6b, 0x1b6b, 230 }, + { 0x1b6c, 0x1b6c, 220 }, + { 0x1b6d, 0x1b73, 230 }, + { 0x1baa, 0x1baa, 9 }, + { 0x1bab, 0x1bab, 9 }, + { 0x1be6, 0x1be6, 7 }, + { 0x1bf2, 0x1bf3, 9 }, + { 0x1c37, 0x1c37, 7 }, + { 0x1cd0, 0x1cd2, 230 }, + { 0x1cd4, 0x1cd4, 1 }, + { 0x1cd5, 0x1cd9, 220 }, + { 0x1cda, 0x1cdb, 230 }, + { 0x1cdc, 0x1cdf, 220 }, + { 0x1ce0, 0x1ce0, 230 }, + { 0x1ce2, 0x1ce8, 1 }, + { 0x1ced, 0x1ced, 220 }, + { 0x1cf4, 0x1cf4, 230 }, + { 0x1cf8, 0x1cf9, 230 }, + { 0x1dc0, 0x1dc1, 230 }, + { 0x1dc2, 0x1dc2, 220 }, + { 0x1dc3, 0x1dc9, 230 }, + { 0x1dca, 0x1dca, 220 }, + { 0x1dcb, 0x1dcc, 230 }, + { 0x1dcd, 0x1dcd, 234 }, + { 0x1dce, 0x1dce, 214 }, + { 0x1dcf, 0x1dcf, 220 }, + { 0x1dd0, 0x1dd0, 202 }, + { 0x1dd1, 0x1df5, 230 }, + { 0x1df6, 0x1df6, 232 }, + { 0x1df7, 0x1df8, 228 }, + { 0x1df9, 0x1df9, 220 }, + { 0x1dfb, 0x1dfb, 230 }, + { 0x1dfc, 0x1dfc, 233 }, + { 0x1dfd, 0x1dfd, 220 }, + { 0x1dfe, 0x1dfe, 230 }, + { 0x1dff, 0x1dff, 220 }, + { 0x20d0, 0x20d1, 230 }, + { 0x20d2, 0x20d3, 1 }, + { 0x20d4, 0x20d7, 230 }, + { 0x20d8, 0x20da, 1 }, + { 0x20db, 0x20dc, 230 }, + { 0x20e1, 0x20e1, 230 }, + { 0x20e5, 0x20e6, 1 }, + { 0x20e7, 0x20e7, 230 }, + { 0x20e8, 0x20e8, 220 }, + { 0x20e9, 0x20e9, 230 }, + { 0x20ea, 0x20eb, 1 }, + { 0x20ec, 0x20ef, 220 }, + { 0x20f0, 0x20f0, 230 }, + { 0x2cef, 0x2cf1, 230 }, + { 0x2d7f, 0x2d7f, 9 }, + { 0x2de0, 0x2dff, 230 }, + { 0x302a, 0x302a, 218 }, + { 0x302b, 0x302b, 228 }, + { 0x302c, 0x302c, 232 }, + { 0x302d, 0x302d, 222 }, + { 0x302e, 0x302f, 224 }, + { 0x3099, 0x309a, 8 }, + { 0xa66f, 0xa66f, 230 }, + { 0xa674, 0xa67d, 230 }, + { 0xa69e, 0xa69f, 230 }, + { 0xa6f0, 0xa6f1, 230 }, + { 0xa806, 0xa806, 9 }, + { 0xa8c4, 0xa8c4, 9 }, + { 0xa8e0, 0xa8f1, 230 }, + { 0xa92b, 0xa92d, 220 }, + { 0xa953, 0xa953, 9 }, + { 0xa9b3, 0xa9b3, 7 }, + { 0xa9c0, 0xa9c0, 9 }, + { 0xaab0, 0xaab0, 230 }, + { 0xaab2, 0xaab3, 230 }, + { 0xaab4, 0xaab4, 220 }, + { 0xaab7, 0xaab8, 230 }, + { 0xaabe, 0xaabf, 230 }, + { 0xaac1, 0xaac1, 230 }, + { 0xaaf6, 0xaaf6, 9 }, + { 0xabed, 0xabed, 9 }, + { 0xfb1e, 0xfb1e, 26 }, + { 0xfe20, 0xfe26, 230 }, + { 0xfe27, 0xfe2d, 220 }, + { 0xfe2e, 0xfe2f, 230 }, +}; + +/* + * Some HFS Plus catalog names contain characters whose canonical + * decomposition either was not yet defined, or was later corrected, in + * the version of Unicode this file's tables above were generated from + * (Apple Technote #1150, itself derived from pre-Unicode-3.0/pre-"Jaguar" + * (Mac OS X 10.2) data). macOS's own fsck_hfs has carried a permanent + * "FixDecomps" fixup for exactly this since 2002 (see + * fsck_hfs.tproj/dfalib/CatalogCheck.c and DecompData.h in the hfsprogs / + * diskdev_cmds sources) and flags any catalog name it finds still using + * the old form as "Illegal name". The two tables and functions below are + * a port of that same fixup data, decoded from hfsprogs 540.1's + * DecompData.h, so this driver stores names the same way macOS has since + * 10.2 rather than the way the original HFS Plus format documentation + * described in 2001. + */ + +/* Category A: single code point -> missing canonical decomposition */ +/* 44 entries */ +struct hfsplus_legacy_decomp hfsplus_legacy_decomp_table[] = { + { 0x01f8, 2, { 0x004e, 0x0300 } }, + { 0x01f9, 2, { 0x006e, 0x0300 } }, + { 0x0218, 2, { 0x0053, 0x0326 } }, + { 0x0219, 2, { 0x0073, 0x0326 } }, + { 0x021a, 2, { 0x0054, 0x0326 } }, + { 0x021b, 2, { 0x0074, 0x0326 } }, + { 0x021e, 2, { 0x0048, 0x030c } }, + { 0x021f, 2, { 0x0068, 0x030c } }, + { 0x0226, 2, { 0x0041, 0x0307 } }, + { 0x0227, 2, { 0x0061, 0x0307 } }, + { 0x0228, 2, { 0x0045, 0x0327 } }, + { 0x0229, 2, { 0x0065, 0x0327 } }, + { 0x022a, 3, { 0x004f, 0x0308, 0x0304 } }, + { 0x022b, 3, { 0x006f, 0x0308, 0x0304 } }, + { 0x022c, 3, { 0x004f, 0x0303, 0x0304 } }, + { 0x022d, 3, { 0x006f, 0x0303, 0x0304 } }, + { 0x022e, 2, { 0x004f, 0x0307 } }, + { 0x022f, 2, { 0x006f, 0x0307 } }, + { 0x0230, 3, { 0x004f, 0x0307, 0x0304 } }, + { 0x0231, 3, { 0x006f, 0x0307, 0x0304 } }, + { 0x0232, 2, { 0x0059, 0x0304 } }, + { 0x0233, 2, { 0x0079, 0x0304 } }, + { 0x0400, 2, { 0x0415, 0x0300 } }, + { 0x040d, 2, { 0x0418, 0x0300 } }, + { 0x0450, 2, { 0x0435, 0x0300 } }, + { 0x045d, 2, { 0x0438, 0x0300 } }, + { 0x04ec, 2, { 0x042d, 0x0308 } }, + { 0x04ed, 2, { 0x044d, 0x0308 } }, + { 0x0622, 2, { 0x0627, 0x0653 } }, + { 0x0623, 2, { 0x0627, 0x0654 } }, + { 0x0624, 2, { 0x0648, 0x0654 } }, + { 0x0625, 2, { 0x0627, 0x0655 } }, + { 0x0626, 2, { 0x064a, 0x0654 } }, + { 0x06c0, 2, { 0x06d5, 0x0654 } }, + { 0x06c2, 2, { 0x06c1, 0x0654 } }, + { 0x06d3, 2, { 0x06d2, 0x0654 } }, + { 0x0a33, 2, { 0x0a32, 0x0a3c } }, + { 0x0a36, 2, { 0x0a38, 0x0a3c } }, + { 0x0dda, 2, { 0x0dd9, 0x0dca } }, + { 0x0ddc, 2, { 0x0dd9, 0x0dcf } }, + { 0x0ddd, 3, { 0x0dd9, 0x0dcf, 0x0dca } }, + { 0x0dde, 2, { 0x0dd9, 0x0ddf } }, + { 0x1026, 2, { 0x1025, 0x102e } }, + { 0xfb1d, 2, { 0x05d9, 0x05b4 } }, +}; + +/* Category B: 2-3 code point sequence -> corrected replacement */ +/* 25 entries */ +struct hfsplus_legacy_seq_fixup hfsplus_legacy_seq_fixups[] = { + { 2, { 0x00a8, 0x030d }, 2, { 0x00a8, 0x0301 } }, + { 2, { 0x0306, 0x0307 }, 1, { 0x0310 } }, + { 2, { 0x0308, 0x030d }, 2, { 0x0308, 0x0301 } }, + { 2, { 0x0391, 0x030d }, 2, { 0x0391, 0x0301 } }, + { 2, { 0x0395, 0x030d }, 2, { 0x0395, 0x0301 } }, + { 2, { 0x0397, 0x030d }, 2, { 0x0397, 0x0301 } }, + { 2, { 0x0399, 0x030d }, 2, { 0x0399, 0x0301 } }, + { 2, { 0x039f, 0x030d }, 2, { 0x039f, 0x0301 } }, + { 2, { 0x03a5, 0x030d }, 2, { 0x03a5, 0x0301 } }, + { 2, { 0x03a9, 0x030d }, 2, { 0x03a9, 0x0301 } }, + { 2, { 0x03b1, 0x030d }, 2, { 0x03b1, 0x0301 } }, + { 2, { 0x03b5, 0x030d }, 2, { 0x03b5, 0x0301 } }, + { 2, { 0x03b7, 0x030d }, 2, { 0x03b7, 0x0301 } }, + { 2, { 0x03b9, 0x030d }, 2, { 0x03b9, 0x0301 } }, + { 2, { 0x03bf, 0x030d }, 2, { 0x03bf, 0x0301 } }, + { 2, { 0x03c5, 0x030d }, 2, { 0x03c5, 0x0301 } }, + { 2, { 0x03c9, 0x030d }, 2, { 0x03c9, 0x0301 } }, + { 2, { 0x03d2, 0x030d }, 2, { 0x03d2, 0x0301 } }, + { 2, { 0x09ac, 0x09bc }, 1, { 0x09b0 } }, + { 2, { 0x0a21, 0x0a3c }, 1, { 0x0a5c } }, + { 2, { 0x0b2f, 0x0b3c }, 1, { 0x0b5f } }, + { 2, { 0x0e4d, 0x0e32 }, 1, { 0x0e33 } }, + { 2, { 0x0ecd, 0x0eb2 }, 1, { 0x0eb3 } }, + { 3, { 0x0fb2, 0x0f80, 0x0f71 }, 1, { 0x0f77 } }, + { 3, { 0x0fb3, 0x0f80, 0x0f71 }, 1, { 0x0f79 } }, +}; + +/* + * Look up the Unicode canonical combining class of a BMP code point. + * Returns 0 ("Not_Reordered") for any code point not listed in the table, + * which includes every ordinary base character. + */ +u8 hfsplus_combining_class(u16 c) +{ + int lo = 0, hi = ARRAY_SIZE(hfsplus_ccc_table) - 1; + + while (lo <= hi) { + int mid = (lo + hi) / 2; + const struct hfsplus_ccc_range *r = &hfsplus_ccc_table[mid]; + + if (c < r->first) + hi = mid - 1; + else if (c > r->last) + lo = mid + 1; + else + return r->combining_class; + } + + return 0; +} + +/* + * Look up the corrected canonical decomposition for a single BMP code + * point that Apple Technote #1150's own decomposition table (above) + * lacks. Returns NULL (and leaves *size alone) if @uc isn't one of these. + */ +const u16 *hfsplus_legacy_decompose(u16 uc, int *size) +{ + int lo = 0, hi = ARRAY_SIZE(hfsplus_legacy_decomp_table) - 1; + + while (lo <= hi) { + int mid = (lo + hi) / 2; + const struct hfsplus_legacy_decomp *e = + &hfsplus_legacy_decomp_table[mid]; + + if (uc < e->uc) + hi = mid - 1; + else if (uc > e->uc) + lo = mid + 1; + else { + *size = e->len; + return e->repl; + } + } + + return NULL; +} + +/* + * Scan an already decomposed and canonically-reordered code unit buffer + * for any of the short legacy sequences above and replace them in place + * with their corrected form. @len is updated to the buffer's new length. + */ +void hfsplus_fixup_legacy_sequences(u16 *buf, int *len) +{ + int i = 0; + + while (i < *len) { + unsigned int j; + bool matched = false; + + for (j = 0; j < ARRAY_SIZE(hfsplus_legacy_seq_fixups); j++) { + struct hfsplus_legacy_seq_fixup *f = + &hfsplus_legacy_seq_fixups[j]; + u16 *src, *dst; + size_t copy_len; + int k; + + if (i + f->match_len > *len) + continue; + for (k = 0; k < f->match_len; k++) + if (buf[i + k] != f->match[k]) + break; + if (k != f->match_len) + continue; + + dst = &buf[i + f->repl_len]; + src = &buf[i + f->match_len]; + copy_len = (*len - i - f->match_len) * sizeof(*buf); + memmove(dst, src, copy_len); + + dst = &buf[i]; + src = f->repl; + copy_len = f->repl_len * sizeof(*buf); + memcpy(dst, src, copy_len); + + *len += f->repl_len - f->match_len; + matched = true; + break; + } + + if (!matched) + i++; + } +} diff --git a/fs/hfsplus/unicode.c b/fs/hfsplus/unicode.c index 008fec186382..59b8788a69a9 100644 --- a/fs/hfsplus/unicode.c +++ b/fs/hfsplus/unicode.c @@ -16,6 +16,7 @@ #include "hfsplus_fs.h" #include "hfsplus_raw.h" +#include "unicode.h" /* Fold the case of a unicode char, given the 16 bit value */ /* Returns folded char, or 0 if ignorable */ @@ -441,45 +442,264 @@ static u16 *decompose_unichar(wchar_t uc, int *size, u16 *hangul_buffer) *size = hfsplus_try_decompose_hangul(uc, result); if (*size == 0) result = hfsplus_decompose_nonhangul(uc, size); + if (!result) { + /* + * Not every character with a canonical decomposition is in + * Apple Technote #1150's own table above; a small, fixed + * set was only added to the decomposition standard (or had + * its decomposition corrected) after that table was + * generated. hfsplus_legacy_decompose() covers those. + */ + const u16 *legacy = hfsplus_legacy_decompose(uc, size); + + if (legacy) + result = memcpy(hangul_buffer, legacy, + *size * sizeof(*legacy)); + } return result; } +/* + * Apply the Unicode Canonical Ordering Algorithm to a decomposed name held + * as plain host-order code units: within each maximal run of characters + * that have a nonzero combining class, stable-sort the run into ascending + * combining-class order. A character with combining class 0 always starts + * a new run and is never itself reordered. + * + * hfsplus_decompose_table decomposes each source character on its own; it + * says nothing about how the decompositions of two different source + * characters should be ordered relative to each other when both produce + * combining marks that end up adjacent. Without this pass, such a + * sequence can be stored in an order that macOS's own fsck_hfs + * (FixDecomps() in CatalogCheck.c) considers illegal, even though every + * individual character was decomposed correctly. + */ +static void hfsplus_canonical_reorder(u16 *ustr, int len) +{ + int i; + + for (i = 1; i < len; i++) { + u8 cls = hfsplus_combining_class(ustr[i]); + int j = i; + + if (!cls) + continue; + + while (j > 0) { + u8 prev_cls = hfsplus_combining_class(ustr[j - 1]); + u16 tmp; + + if (!prev_cls || prev_cls <= cls) + break; + + tmp = ustr[j]; + ustr[j] = ustr[j - 1]; + ustr[j - 1] = tmp; + j--; + } + } +} + +#define HFSPLUS_HANGUL_MAX_JAMO (3) /* L + V + optional T */ + +static_assert(HFSPLUS_HANGUL_MAX_JAMO >= HFSPLUS_LEGACY_DECOMP_MAX_LEN, + "hangul_buffer must fit the longest legacy decomposition too"); + +/* + * Decompose and canonically reorder an entire Linux name, as plain + * host-order code units. hfsplus_asc2uni(), hfsplus_hash_dentry() and + * hfsplus_compare_dentry() all go through this so that storage, hashing + * and comparison always agree on what a given name canonicalizes to. + * + * @out must hold at least @max_len entries, which must not exceed + * HFSPLUS_MAX_STRLEN. Returns the number of code units written. If + * @consumed is non-NULL, it is set to the number of input bytes actually + * consumed, which is less than @len when @out fills up first. + */ +static int hfsplus_decompose_str(struct super_block *sb, const char *astr, + int len, int max_len, int name_type, + u16 *out, int *consumed) +{ + int decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); + const char *start = astr; + int outlen = 0; + + while (outlen < max_len && len > 0) { + u16 *dstr; + u16 dhangul[HFSPLUS_HANGUL_MAX_JAMO]; + int dsize, size; + wchar_t c; + + size = asc2unichar(sb, astr, len, &c, name_type); + + dstr = decompose ? decompose_unichar(c, &dsize, dhangul) : NULL; + if (dstr) { + if (outlen + dsize > max_len) + break; + do { + out[outlen++] = *dstr++; + } while (--dsize > 0); + } else { + out[outlen++] = c; + } + + astr += size; + len -= size; + } + + hfsplus_canonical_reorder(out, outlen); + hfsplus_fixup_legacy_sequences(out, &outlen); + if (consumed) + *consumed = astr - start; + return outlen; +} + int hfsplus_asc2uni(struct super_block *sb, struct hfsplus_unistr *ustr, int max_unistr_len, const char *astr, int len, int name_type) { - int size, dsize, decompose; - u16 *dstr, outlen = 0; - wchar_t c; - u16 dhangul[3]; + u16 buf[HFSPLUS_MAX_STRLEN]; + int outlen, i, consumed; - decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); - while (outlen < max_unistr_len && len > 0) { - size = asc2unichar(sb, astr, len, &c, name_type); + if (max_unistr_len > HFSPLUS_MAX_STRLEN) + max_unistr_len = HFSPLUS_MAX_STRLEN; - if (decompose) - dstr = decompose_unichar(c, &dsize, dhangul); - else - dstr = NULL; - if (dstr) { - if (outlen + dsize > max_unistr_len) - break; - do { - ustr->unicode[outlen++] = cpu_to_be16(*dstr++); - } while (--dsize > 0); - } else - ustr->unicode[outlen++] = cpu_to_be16(c); - - astr += size; - len -= size; - } + outlen = hfsplus_decompose_str(sb, astr, len, max_unistr_len, + name_type, buf, &consumed); + for (i = 0; i < outlen; i++) + ustr->unicode[i] = cpu_to_be16(buf[i]); ustr->length = cpu_to_be16(outlen); - if (len > 0) + + if (consumed < len) return -ENAMETOOLONG; return 0; } EXPORT_SYMBOL_IF_KUNIT(hfsplus_asc2uni); +/* + * Maximum length of a single maximal run of nonzero-combining-class code + * units that hfsplus_decompose_iter_next() below will canonically + * reorder. Every code unit with combining class 0 starts a new run, so + * this only bounds how many *consecutive* combining marks between two + * base characters get sorted - real text, and even deliberately + * adversarial "Zalgo" text, essentially never approaches this. Keeping + * it small means hfsplus_hash_dentry() and hfsplus_compare_dentry() only + * ever need a tiny amount of lookahead state, rather than buffering an + * entire (up to 255-unit) name. + */ +#define HFSPLUS_CCC_RUN_MAX 32 + +/* + * Iterator that produces the canonically-ordered, decomposed code units + * of a Linux name one at a time, without materializing the whole name. + * hfsplus_hash_dentry() and hfsplus_compare_dentry() use this so that + * hashing and comparison always agree with what hfsplus_asc2uni() would + * actually store, while still comparing lazily (stopping at the first + * difference) the way this code did before canonical reordering existed. + */ +struct hfsplus_decompose_iter { + struct super_block *sb; + const char *astr; + int len; + int name_type; + int decompose; + + u16 run[HFSPLUS_CCC_RUN_MAX]; + int run_len; + int run_pos; +}; + +static void hfsplus_decompose_iter_init(struct hfsplus_decompose_iter *it, + struct super_block *sb, + const char *astr, int len, + int name_type) +{ + it->sb = sb; + it->astr = astr; + it->len = len; + it->name_type = name_type; + it->decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); + it->run_len = 0; + it->run_pos = 0; +} + +/* + * Gather the next maximal run of nonzero-combining-class code units + * (starting with whatever character comes next, base or not) and + * canonically reorder just that run. A character is only included once + * we know the class of the first unit it produces, so decoding it is + * speculative until that's decided. + */ +static bool hfsplus_decompose_iter_refill(struct hfsplus_decompose_iter *it) +{ + it->run_pos = 0; + it->run_len = 0; + + while (it->len > 0) { + u16 *dstr; + u16 dhangul[HFSPLUS_HANGUL_MAX_JAMO]; + int dsize, size; + wchar_t c; + + size = asc2unichar(it->sb, it->astr, it->len, &c, + it->name_type); + + dstr = it->decompose ? + decompose_unichar(c, &dsize, dhangul) : NULL; + if (!dstr) { + dhangul[0] = c; + dstr = dhangul; + dsize = 1; + } + + if (it->run_len > 0 && !hfsplus_combining_class(dstr[0])) + break; + + if (it->run_len + dsize > HFSPLUS_CCC_RUN_MAX) + break; + + it->astr += size; + it->len -= size; + + do { + it->run[it->run_len++] = *dstr++; + } while (--dsize > 0); + } + + hfsplus_canonical_reorder(it->run, it->run_len); + hfsplus_fixup_legacy_sequences(it->run, &it->run_len); + return it->run_len > 0; +} + +/* Returns the next code unit, or a negative value once @it is exhausted. */ +static int hfsplus_decompose_iter_next(struct hfsplus_decompose_iter *it) +{ + if (it->run_pos >= it->run_len && !hfsplus_decompose_iter_refill(it)) + return -1; + return it->run[it->run_pos++]; +} + +/* + * Returns the next code unit that matters for comparison/hashing: folded + * if @casefold, with any character folding to 0 ("ignorable") skipped + * entirely. Returns a negative value once @it is exhausted. + */ +static int hfsplus_decompose_iter_next_folded(struct hfsplus_decompose_iter *it, + int casefold) +{ + int c; + + do { + c = hfsplus_decompose_iter_next(it); + if (c < 0) + return -1; + if (casefold) + c = case_fold(c); + } while (casefold && !c); + + return c; +} + /* * Hash a string to an integer as appropriate for the HFS+ filesystem. * Composed unicode characters are decomposed and case-folding is performed @@ -488,45 +708,17 @@ EXPORT_SYMBOL_IF_KUNIT(hfsplus_asc2uni); int hfsplus_hash_dentry(const struct dentry *dentry, struct qstr *str) { struct super_block *sb = dentry->d_sb; - const char *astr; - const u16 *dstr; - int casefold, decompose, size, len; + int casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); + struct hfsplus_decompose_iter it; unsigned long hash; - wchar_t c; - u16 c2; - u16 dhangul[3]; + int c; + + hfsplus_decompose_iter_init(&it, sb, str->name, str->len, + HFS_REGULAR_NAME); - casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); - decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); hash = init_name_hash(dentry); - astr = str->name; - len = str->len; - while (len > 0) { - int dsize; - size = asc2unichar(sb, astr, len, &c, HFS_REGULAR_NAME); - astr += size; - len -= size; - - if (decompose) - dstr = decompose_unichar(c, &dsize, dhangul); - else - dstr = NULL; - if (dstr) { - do { - c2 = *dstr++; - if (casefold) - c2 = case_fold(c2); - if (!casefold || c2) - hash = partial_name_hash(c2, hash); - } while (--dsize > 0); - } else { - c2 = c; - if (casefold) - c2 = case_fold(c2); - if (!casefold || c2) - hash = partial_name_hash(c2, hash); - } - } + while ((c = hfsplus_decompose_iter_next_folded(&it, casefold)) >= 0) + hash = partial_name_hash(c, hash); str->hash = end_name_hash(hash); return 0; @@ -542,87 +734,21 @@ int hfsplus_compare_dentry(const struct dentry *dentry, unsigned int len, const char *str, const struct qstr *name) { struct super_block *sb = dentry->d_sb; - int casefold, decompose, size; - int dsize1, dsize2, len1, len2; - const u16 *dstr1, *dstr2; - const char *astr1, *astr2; - u16 c1, c2; - wchar_t c; - u16 dhangul_1[3], dhangul_2[3]; + int casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); + struct hfsplus_decompose_iter it1, it2; - casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); - decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); - astr1 = str; - len1 = len; - astr2 = name->name; - len2 = name->len; - dsize1 = dsize2 = 0; - dstr1 = dstr2 = NULL; + hfsplus_decompose_iter_init(&it1, sb, str, len, HFS_REGULAR_NAME); + hfsplus_decompose_iter_init(&it2, sb, name->name, name->len, + HFS_REGULAR_NAME); - while (len1 > 0 && len2 > 0) { - if (!dsize1) { - size = asc2unichar(sb, astr1, len1, &c, - HFS_REGULAR_NAME); - astr1 += size; - len1 -= size; + while (1) { + int c1 = hfsplus_decompose_iter_next_folded(&it1, casefold); + int c2 = hfsplus_decompose_iter_next_folded(&it2, casefold); - if (decompose) - dstr1 = decompose_unichar(c, &dsize1, - dhangul_1); - if (!decompose || !dstr1) { - c1 = c; - dstr1 = &c1; - dsize1 = 1; - } - } - - if (!dsize2) { - size = asc2unichar(sb, astr2, len2, &c, - HFS_REGULAR_NAME); - astr2 += size; - len2 -= size; - - if (decompose) - dstr2 = decompose_unichar(c, &dsize2, - dhangul_2); - if (!decompose || !dstr2) { - c2 = c; - dstr2 = &c2; - dsize2 = 1; - } - } - - c1 = *dstr1; - c2 = *dstr2; - if (casefold) { - c1 = case_fold(c1); - if (!c1) { - dstr1++; - dsize1--; - continue; - } - c2 = case_fold(c2); - if (!c2) { - dstr2++; - dsize2--; - continue; - } - } - if (c1 < c2) - return -1; - else if (c1 > c2) - return 1; - - dstr1++; - dsize1--; - dstr2++; - dsize2--; + if (c1 < 0 || c2 < 0) + return c1 == c2 ? 0 : (c1 < 0 ? -1 : 1); + if (c1 != c2) + return c1 < c2 ? -1 : 1; } - - if (len1 < len2) - return -1; - if (len1 > len2) - return 1; - return 0; } EXPORT_SYMBOL_IF_KUNIT(hfsplus_compare_dentry); diff --git a/fs/hfsplus/unicode.h b/fs/hfsplus/unicode.h new file mode 100644 index 000000000000..31cc7691c916 --- /dev/null +++ b/fs/hfsplus/unicode.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Unicode related declarations + */ + +#ifndef _LINUX_HFSPLUS_UNICODE_H +#define _LINUX_HFSPLUS_UNICODE_H + +/* + * struct hfsplus_ccc_range - one run of consecutive BMP code points that + * share the same Unicode canonical combining class + * @first: the first UTF-16 code point in the run + * @last: the last code point in the run (inclusive) + * @combining_class: canonical combining class (ccc) shared by @first..@last + */ +struct hfsplus_ccc_range { + u16 first; + u16 last; + u8 combining_class; +}; + +/* + * struct hfsplus_legacy_decomp - corrected canonical decomposition + * @uc: the code point (a UTF-16 code unit) to look up + * @len: number of valid code units in @repl + * @repl: corrected decomposition sequence (base letter + combining marks) + */ +struct hfsplus_legacy_decomp { + u16 uc; + u8 len; +#define HFSPLUS_LEGACY_DECOMP_MAX_LEN (3) + u16 repl[HFSPLUS_LEGACY_DECOMP_MAX_LEN]; +}; + +/* + * struct hfsplus_legacy_seq_fixup - corrected replacement for a short + * sequence of already-decomposed code units + * @match_len: number of valid code units in @match + * @match: decomposed sequence to look for in the buffer + * @repl_len: number of valid code units in @repl + * @repl: corrected sequence to substitute in place of @match + */ +struct hfsplus_legacy_seq_fixup { + u8 match_len; +#define HFSPLUS_LEGACY_SEQ_MAX_MATCH (3) + u16 match[HFSPLUS_LEGACY_SEQ_MAX_MATCH]; + u8 repl_len; +#define HFSPLUS_LEGACY_SEQ_MAX_REPL (2) + u16 repl[HFSPLUS_LEGACY_SEQ_MAX_REPL]; +}; + +extern u16 hfsplus_case_fold_table[]; +extern u16 hfsplus_decompose_table[]; +extern u16 hfsplus_compose_table[]; +extern struct hfsplus_ccc_range hfsplus_ccc_table[]; +extern struct hfsplus_legacy_decomp hfsplus_legacy_decomp_table[]; +extern struct hfsplus_legacy_seq_fixup hfsplus_legacy_seq_fixups[]; + +u8 hfsplus_combining_class(u16 c); +const u16 *hfsplus_legacy_decompose(u16 uc, int *size); +void hfsplus_fixup_legacy_sequences(u16 *buf, int *len); + +#endif /* _LINUX_HFSPLUS_UNICODE_H */ diff --git a/fs/hfsplus/unicode_test.c b/fs/hfsplus/unicode_test.c index 7f6b2a3c69d9..a683c6d5fc22 100644 --- a/fs/hfsplus/unicode_test.c +++ b/fs/hfsplus/unicode_test.c @@ -10,6 +10,7 @@ #include #include #include "hfsplus_fs.h" +#include "unicode.h" struct test_mock_string_env { struct hfsplus_unistr str1; @@ -947,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; @@ -1230,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) { @@ -1552,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), @@ -1567,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), @@ -1580,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), {} };