// SPDX-License-Identifier: GPL-2.0 /* * linux/fs/hfsplus/unicode.c * * Copyright (C) 2001 * Brad Boyer (flar@allandria.com) * (C) 2003 Ardis Technologies * * Handler routines for unicode strings */ #include #include #include #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 */ static inline u16 case_fold(u16 c) { u16 tmp; tmp = hfsplus_case_fold_table[c >> 8]; if (tmp) tmp = hfsplus_case_fold_table[tmp + (c & 0xff)]; else tmp = c; return tmp; } /* Compare unicode strings, return values like normal strcmp */ int hfsplus_strcasecmp(const struct hfsplus_unistr *s1, const struct hfsplus_unistr *s2) { u16 len1, len2, c1, c2; const hfsplus_unichr *p1, *p2; len1 = be16_to_cpu(s1->length); len2 = be16_to_cpu(s2->length); p1 = s1->unicode; p2 = s2->unicode; if (len1 > HFSPLUS_MAX_STRLEN) { len1 = HFSPLUS_MAX_STRLEN; pr_err("invalid length %u has been corrected to %d\n", be16_to_cpu(s1->length), len1); } if (len2 > HFSPLUS_MAX_STRLEN) { len2 = HFSPLUS_MAX_STRLEN; pr_err("invalid length %u has been corrected to %d\n", be16_to_cpu(s2->length), len2); } while (1) { c1 = c2 = 0; while (len1 && !c1) { c1 = case_fold(be16_to_cpu(*p1)); p1++; len1--; } while (len2 && !c2) { c2 = case_fold(be16_to_cpu(*p2)); p2++; len2--; } if (c1 != c2) return (c1 < c2) ? -1 : 1; if (!c1 && !c2) return 0; } } EXPORT_SYMBOL_IF_KUNIT(hfsplus_strcasecmp); /* Compare names as a sequence of 16-bit unsigned integers */ int hfsplus_strcmp(const struct hfsplus_unistr *s1, const struct hfsplus_unistr *s2) { u16 len1, len2, c1, c2; const hfsplus_unichr *p1, *p2; int len; len1 = be16_to_cpu(s1->length); len2 = be16_to_cpu(s2->length); p1 = s1->unicode; p2 = s2->unicode; if (len1 > HFSPLUS_MAX_STRLEN) { len1 = HFSPLUS_MAX_STRLEN; pr_err("invalid length %u has been corrected to %d\n", be16_to_cpu(s1->length), len1); } if (len2 > HFSPLUS_MAX_STRLEN) { len2 = HFSPLUS_MAX_STRLEN; pr_err("invalid length %u has been corrected to %d\n", be16_to_cpu(s2->length), len2); } for (len = min(len1, len2); len > 0; len--) { c1 = be16_to_cpu(*p1); c2 = be16_to_cpu(*p2); if (c1 != c2) return c1 < c2 ? -1 : 1; p1++; p2++; } return len1 < len2 ? -1 : len1 > len2 ? 1 : 0; } EXPORT_SYMBOL_IF_KUNIT(hfsplus_strcmp); #define Hangul_SBase 0xac00 #define Hangul_LBase 0x1100 #define Hangul_VBase 0x1161 #define Hangul_TBase 0x11a7 #define Hangul_SCount 11172 #define Hangul_LCount 19 #define Hangul_VCount 21 #define Hangul_TCount 28 #define Hangul_NCount (Hangul_VCount * Hangul_TCount) static u16 *hfsplus_compose_lookup(u16 *p, u16 cc) { int i, s, e; s = 1; e = p[1]; if (!e || cc < p[s * 2] || cc > p[e * 2]) return NULL; do { i = (s + e) / 2; if (cc > p[i * 2]) s = i + 1; else if (cc < p[i * 2]) e = i - 1; else return hfsplus_compose_table + p[i * 2 + 1]; } while (s <= e); return NULL; } /* * In HFS+, a filename can contain / because : is the separator. * The slash is a valid filename character on macOS. * But on Linux, / is the path separator and * it cannot appear in a filename component. * There's a parallel mapping for the NUL character (0 -> U+2400). * NUL terminates strings in C/POSIX but is valid in HFS+ filenames. */ static inline void hfsplus_mac2linux_compatibility_check(u16 symbol, u16 *conversion, int name_type) { *conversion = symbol; switch (name_type) { case HFS_XATTR_NAME: /* ignore conversion */ return; default: /* continue logic */ break; } switch (symbol) { case 0: *conversion = 0x2400; break; case '/': *conversion = ':'; break; } } static int hfsplus_uni2asc(struct super_block *sb, const struct hfsplus_unistr *ustr, int max_len, char *astr, int *len_p, int name_type) { const hfsplus_unichr *ip; struct nls_table *nls = HFSPLUS_SB(sb)->nls; u8 *op; u16 cc, c0, c1; u16 *ce1, *ce2; int i, len, ustrlen, res, compose; op = astr; ip = ustr->unicode; ustrlen = be16_to_cpu(ustr->length); if (ustrlen > max_len) { ustrlen = max_len; pr_err("invalid length %u has been corrected to %d\n", be16_to_cpu(ustr->length), ustrlen); } len = *len_p; ce1 = NULL; compose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); while (ustrlen > 0) { c0 = be16_to_cpu(*ip++); ustrlen--; /* search for single decomposed char */ if (likely(compose)) ce1 = hfsplus_compose_lookup(hfsplus_compose_table, c0); if (ce1) cc = ce1[0]; else cc = 0; if (cc) { /* start of a possibly decomposed Hangul char */ if (cc != 0xffff) goto done; if (!ustrlen) goto same; c1 = be16_to_cpu(*ip) - Hangul_VBase; if (c1 < Hangul_VCount) { /* compose the Hangul char */ cc = (c0 - Hangul_LBase) * Hangul_VCount; cc = (cc + c1) * Hangul_TCount; cc += Hangul_SBase; ip++; ustrlen--; if (!ustrlen) goto done; c1 = be16_to_cpu(*ip) - Hangul_TBase; if (c1 > 0 && c1 < Hangul_TCount) { cc += c1; ip++; ustrlen--; } goto done; } } while (1) { /* main loop for common case of not composed chars */ if (!ustrlen) goto same; c1 = be16_to_cpu(*ip); if (likely(compose)) ce1 = hfsplus_compose_lookup( hfsplus_compose_table, c1); if (ce1) break; hfsplus_mac2linux_compatibility_check(c0, &c0, name_type); res = nls->uni2char(c0, op, len); if (res < 0) { if (res == -ENAMETOOLONG) goto out; *op = '?'; res = 1; } op += res; len -= res; c0 = c1; ip++; ustrlen--; } ce2 = hfsplus_compose_lookup(ce1, c0); if (ce2) { i = 1; while (i < ustrlen) { ce1 = hfsplus_compose_lookup(ce2, be16_to_cpu(ip[i])); if (!ce1) break; i++; ce2 = ce1; } cc = ce2[0]; if (cc) { ip += i; ustrlen -= i; goto done; } } same: hfsplus_mac2linux_compatibility_check(c0, &cc, name_type); done: res = nls->uni2char(cc, op, len); if (res < 0) { if (res == -ENAMETOOLONG) goto out; *op = '?'; res = 1; } op += res; len -= res; } res = 0; out: *len_p = (char *)op - astr; return res; } inline int hfsplus_uni2asc_str(struct super_block *sb, const struct hfsplus_unistr *ustr, char *astr, int *len_p) { return hfsplus_uni2asc(sb, ustr, HFSPLUS_MAX_STRLEN, astr, len_p, HFS_REGULAR_NAME); } EXPORT_SYMBOL_IF_KUNIT(hfsplus_uni2asc_str); inline int hfsplus_uni2asc_xattr_str(struct super_block *sb, const struct hfsplus_attr_unistr *ustr, char *astr, int *len_p) { return hfsplus_uni2asc(sb, (const struct hfsplus_unistr *)ustr, HFSPLUS_ATTR_MAX_STRLEN, astr, len_p, HFS_XATTR_NAME); } EXPORT_SYMBOL_IF_KUNIT(hfsplus_uni2asc_xattr_str); /* * In HFS+, a filename can contain / because : is the separator. * The slash is a valid filename character on macOS. * But on Linux, / is the path separator and * it cannot appear in a filename component. * There's a parallel mapping for the NUL character (0 -> U+2400). * NUL terminates strings in C/POSIX but is valid in HFS+ filenames. */ static inline void hfsplus_linux2mac_compatibility_check(wchar_t *uc, int name_type) { switch (name_type) { case HFS_XATTR_NAME: /* ignore conversion */ return; default: /* continue logic */ break; } switch (*uc) { case 0x2400: *uc = 0; break; case ':': *uc = '/'; break; } } /* * Convert one or more ASCII characters into a single unicode character. * Returns the number of ASCII characters corresponding to the unicode char. */ static inline int asc2unichar(struct super_block *sb, const char *astr, int len, wchar_t *uc, int name_type) { int size = HFSPLUS_SB(sb)->nls->char2uni(astr, len, uc); if (size <= 0) { *uc = '?'; size = 1; } hfsplus_linux2mac_compatibility_check(uc, name_type); return size; } /* Decomposes a non-Hangul unicode character. */ static u16 *hfsplus_decompose_nonhangul(wchar_t uc, int *size) { int off; off = hfsplus_decompose_table[(uc >> 12) & 0xf]; if (off == 0 || off == 0xffff) return NULL; off = hfsplus_decompose_table[off + ((uc >> 8) & 0xf)]; if (!off) return NULL; off = hfsplus_decompose_table[off + ((uc >> 4) & 0xf)]; if (!off) return NULL; off = hfsplus_decompose_table[off + (uc & 0xf)]; *size = off & 3; if (*size == 0) return NULL; return hfsplus_decompose_table + (off / 4); } /* * Try to decompose a unicode character as Hangul. Return 0 if @uc is not * precomposed Hangul, otherwise return the length of the decomposition. * * This function was adapted from sample code from the Unicode Standard * Annex #15: Unicode Normalization Forms, version 3.2.0. * * Copyright (C) 1991-2018 Unicode, Inc. All rights reserved. Distributed * under the Terms of Use in http://www.unicode.org/copyright.html. */ static int hfsplus_try_decompose_hangul(wchar_t uc, u16 *result) { int index; int l, v, t; index = uc - Hangul_SBase; if (index < 0 || index >= Hangul_SCount) return 0; l = Hangul_LBase + index / Hangul_NCount; v = Hangul_VBase + (index % Hangul_NCount) / Hangul_TCount; t = Hangul_TBase + index % Hangul_TCount; result[0] = l; result[1] = v; if (t != Hangul_TBase) { result[2] = t; return 3; } return 2; } /* Decomposes a single unicode character. */ static u16 *decompose_unichar(wchar_t uc, int *size, u16 *hangul_buffer) { u16 *result; /* Hangul is handled separately */ result = 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) { u16 buf[HFSPLUS_MAX_STRLEN]; int outlen, i, consumed; if (max_unistr_len > HFSPLUS_MAX_STRLEN) max_unistr_len = HFSPLUS_MAX_STRLEN; 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 (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 * if the appropriate bits are (un)set on the superblock. */ int hfsplus_hash_dentry(const struct dentry *dentry, struct qstr *str) { struct super_block *sb = dentry->d_sb; int casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); struct hfsplus_decompose_iter it; unsigned long hash; int c; hfsplus_decompose_iter_init(&it, sb, str->name, str->len, HFS_REGULAR_NAME); hash = init_name_hash(dentry); while ((c = hfsplus_decompose_iter_next_folded(&it, casefold)) >= 0) hash = partial_name_hash(c, hash); str->hash = end_name_hash(hash); return 0; } EXPORT_SYMBOL_IF_KUNIT(hfsplus_hash_dentry); /* * Compare strings with HFS+ filename ordering. * Composed unicode characters are decomposed and case-folding is performed * if the appropriate bits are (un)set on the superblock. */ 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 = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); struct hfsplus_decompose_iter it1, it2; 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 (1) { int c1 = hfsplus_decompose_iter_next_folded(&it1, casefold); int c2 = hfsplus_decompose_iter_next_folded(&it2, casefold); if (c1 < 0 || c2 < 0) return c1 == c2 ? 0 : (c1 < 0 ? -1 : 1); if (c1 != c2) return c1 < c2 ? -1 : 1; } } EXPORT_SYMBOL_IF_KUNIT(hfsplus_compare_dentry);