mirror of
https://github.com/torvalds/linux
synced 2024-11-05 18:23:50 +00:00
b886ee3e77
This patch implements the actual support for case-insensitive file name lookups in ext4, based on the feature bit and the encoding stored in the superblock. A filesystem that has the casefold feature set is able to configure directories with the +F (EXT4_CASEFOLD_FL) attribute, enabling lookups to succeed in that directory in a case-insensitive fashion, i.e: match a directory entry even if the name used by userspace is not a byte per byte match with the disk name, but is an equivalent case-insensitive version of the Unicode string. This operation is called a case-insensitive file name lookup. The feature is configured as an inode attribute applied to directories and inherited by its children. This attribute can only be enabled on empty directories for filesystems that support the encoding feature, thus preventing collision of file names that only differ by case. * dcache handling: For a +F directory, Ext4 only stores the first equivalent name dentry used in the dcache. This is done to prevent unintentional duplication of dentries in the dcache, while also allowing the VFS code to quickly find the right entry in the cache despite which equivalent string was used in a previous lookup, without having to resort to ->lookup(). d_hash() of casefolded directories is implemented as the hash of the casefolded string, such that we always have a well-known bucket for all the equivalencies of the same string. d_compare() uses the utf8_strncasecmp() infrastructure, which handles the comparison of equivalent, same case, names as well. For now, negative lookups are not inserted in the dcache, since they would need to be invalidated anyway, because we can't trust missing file dentries. This is bad for performance but requires some leveraging of the vfs layer to fix. We can live without that for now, and so does everyone else. * on-disk data: Despite using a specific version of the name as the internal representation within the dcache, the name stored and fetched from the disk is a byte-per-byte match with what the user requested, making this implementation 'name-preserving'. i.e. no actual information is lost when writing to storage. DX is supported by modifying the hashes used in +F directories to make them case/encoding-aware. The new disk hashes are calculated as the hash of the full casefolded string, instead of the string directly. This allows us to efficiently search for file names in the htree without requiring the user to provide an exact name. * Dealing with invalid sequences: By default, when a invalid UTF-8 sequence is identified, ext4 will treat it as an opaque byte sequence, ignoring the encoding and reverting to the old behavior for that unique file. This means that case-insensitive file name lookup will not work only for that file. An optional bit can be set in the superblock telling the filesystem code and userspace tools to enforce the encoding. When that optional bit is set, any attempt to create a file name using an invalid UTF-8 sequence will fail and return an error to userspace. * Normalization algorithm: The UTF-8 algorithms used to compare strings in ext4 is implemented lives in fs/unicode, and is based on a previous version developed by SGI. It implements the Canonical decomposition (NFD) algorithm described by the Unicode specification 12.1, or higher, combined with the elimination of ignorable code points (NFDi) and full case-folding (CF) as documented in fs/unicode/utf8_norm.c. NFD seems to be the best normalization method for EXT4 because: - It has a lower cost than NFC/NFKC (which requires decomposing to NFD as an intermediary step) - It doesn't eliminate important semantic meaning like compatibility decompositions. Although: - This implementation is not completely linguistic accurate, because different languages have conflicting rules, which would require the specialization of the filesystem to a given locale, which brings all sorts of problems for removable media and for users who use more than one language. Signed-off-by: Gabriel Krisman Bertazi <krisman@collabora.co.uk> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
302 lines
6.8 KiB
C
302 lines
6.8 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* linux/fs/ext4/hash.c
|
|
*
|
|
* Copyright (C) 2002 by Theodore Ts'o
|
|
*/
|
|
|
|
#include <linux/fs.h>
|
|
#include <linux/unicode.h>
|
|
#include <linux/compiler.h>
|
|
#include <linux/bitops.h>
|
|
#include "ext4.h"
|
|
|
|
#define DELTA 0x9E3779B9
|
|
|
|
static void TEA_transform(__u32 buf[4], __u32 const in[])
|
|
{
|
|
__u32 sum = 0;
|
|
__u32 b0 = buf[0], b1 = buf[1];
|
|
__u32 a = in[0], b = in[1], c = in[2], d = in[3];
|
|
int n = 16;
|
|
|
|
do {
|
|
sum += DELTA;
|
|
b0 += ((b1 << 4)+a) ^ (b1+sum) ^ ((b1 >> 5)+b);
|
|
b1 += ((b0 << 4)+c) ^ (b0+sum) ^ ((b0 >> 5)+d);
|
|
} while (--n);
|
|
|
|
buf[0] += b0;
|
|
buf[1] += b1;
|
|
}
|
|
|
|
/* F, G and H are basic MD4 functions: selection, majority, parity */
|
|
#define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z))))
|
|
#define G(x, y, z) (((x) & (y)) + (((x) ^ (y)) & (z)))
|
|
#define H(x, y, z) ((x) ^ (y) ^ (z))
|
|
|
|
/*
|
|
* The generic round function. The application is so specific that
|
|
* we don't bother protecting all the arguments with parens, as is generally
|
|
* good macro practice, in favor of extra legibility.
|
|
* Rotation is separate from addition to prevent recomputation
|
|
*/
|
|
#define ROUND(f, a, b, c, d, x, s) \
|
|
(a += f(b, c, d) + x, a = rol32(a, s))
|
|
#define K1 0
|
|
#define K2 013240474631UL
|
|
#define K3 015666365641UL
|
|
|
|
/*
|
|
* Basic cut-down MD4 transform. Returns only 32 bits of result.
|
|
*/
|
|
static __u32 half_md4_transform(__u32 buf[4], __u32 const in[8])
|
|
{
|
|
__u32 a = buf[0], b = buf[1], c = buf[2], d = buf[3];
|
|
|
|
/* Round 1 */
|
|
ROUND(F, a, b, c, d, in[0] + K1, 3);
|
|
ROUND(F, d, a, b, c, in[1] + K1, 7);
|
|
ROUND(F, c, d, a, b, in[2] + K1, 11);
|
|
ROUND(F, b, c, d, a, in[3] + K1, 19);
|
|
ROUND(F, a, b, c, d, in[4] + K1, 3);
|
|
ROUND(F, d, a, b, c, in[5] + K1, 7);
|
|
ROUND(F, c, d, a, b, in[6] + K1, 11);
|
|
ROUND(F, b, c, d, a, in[7] + K1, 19);
|
|
|
|
/* Round 2 */
|
|
ROUND(G, a, b, c, d, in[1] + K2, 3);
|
|
ROUND(G, d, a, b, c, in[3] + K2, 5);
|
|
ROUND(G, c, d, a, b, in[5] + K2, 9);
|
|
ROUND(G, b, c, d, a, in[7] + K2, 13);
|
|
ROUND(G, a, b, c, d, in[0] + K2, 3);
|
|
ROUND(G, d, a, b, c, in[2] + K2, 5);
|
|
ROUND(G, c, d, a, b, in[4] + K2, 9);
|
|
ROUND(G, b, c, d, a, in[6] + K2, 13);
|
|
|
|
/* Round 3 */
|
|
ROUND(H, a, b, c, d, in[3] + K3, 3);
|
|
ROUND(H, d, a, b, c, in[7] + K3, 9);
|
|
ROUND(H, c, d, a, b, in[2] + K3, 11);
|
|
ROUND(H, b, c, d, a, in[6] + K3, 15);
|
|
ROUND(H, a, b, c, d, in[1] + K3, 3);
|
|
ROUND(H, d, a, b, c, in[5] + K3, 9);
|
|
ROUND(H, c, d, a, b, in[0] + K3, 11);
|
|
ROUND(H, b, c, d, a, in[4] + K3, 15);
|
|
|
|
buf[0] += a;
|
|
buf[1] += b;
|
|
buf[2] += c;
|
|
buf[3] += d;
|
|
|
|
return buf[1]; /* "most hashed" word */
|
|
}
|
|
#undef ROUND
|
|
#undef K1
|
|
#undef K2
|
|
#undef K3
|
|
#undef F
|
|
#undef G
|
|
#undef H
|
|
|
|
/* The old legacy hash */
|
|
static __u32 dx_hack_hash_unsigned(const char *name, int len)
|
|
{
|
|
__u32 hash, hash0 = 0x12a3fe2d, hash1 = 0x37abe8f9;
|
|
const unsigned char *ucp = (const unsigned char *) name;
|
|
|
|
while (len--) {
|
|
hash = hash1 + (hash0 ^ (((int) *ucp++) * 7152373));
|
|
|
|
if (hash & 0x80000000)
|
|
hash -= 0x7fffffff;
|
|
hash1 = hash0;
|
|
hash0 = hash;
|
|
}
|
|
return hash0 << 1;
|
|
}
|
|
|
|
static __u32 dx_hack_hash_signed(const char *name, int len)
|
|
{
|
|
__u32 hash, hash0 = 0x12a3fe2d, hash1 = 0x37abe8f9;
|
|
const signed char *scp = (const signed char *) name;
|
|
|
|
while (len--) {
|
|
hash = hash1 + (hash0 ^ (((int) *scp++) * 7152373));
|
|
|
|
if (hash & 0x80000000)
|
|
hash -= 0x7fffffff;
|
|
hash1 = hash0;
|
|
hash0 = hash;
|
|
}
|
|
return hash0 << 1;
|
|
}
|
|
|
|
static void str2hashbuf_signed(const char *msg, int len, __u32 *buf, int num)
|
|
{
|
|
__u32 pad, val;
|
|
int i;
|
|
const signed char *scp = (const signed char *) msg;
|
|
|
|
pad = (__u32)len | ((__u32)len << 8);
|
|
pad |= pad << 16;
|
|
|
|
val = pad;
|
|
if (len > num*4)
|
|
len = num * 4;
|
|
for (i = 0; i < len; i++) {
|
|
val = ((int) scp[i]) + (val << 8);
|
|
if ((i % 4) == 3) {
|
|
*buf++ = val;
|
|
val = pad;
|
|
num--;
|
|
}
|
|
}
|
|
if (--num >= 0)
|
|
*buf++ = val;
|
|
while (--num >= 0)
|
|
*buf++ = pad;
|
|
}
|
|
|
|
static void str2hashbuf_unsigned(const char *msg, int len, __u32 *buf, int num)
|
|
{
|
|
__u32 pad, val;
|
|
int i;
|
|
const unsigned char *ucp = (const unsigned char *) msg;
|
|
|
|
pad = (__u32)len | ((__u32)len << 8);
|
|
pad |= pad << 16;
|
|
|
|
val = pad;
|
|
if (len > num*4)
|
|
len = num * 4;
|
|
for (i = 0; i < len; i++) {
|
|
val = ((int) ucp[i]) + (val << 8);
|
|
if ((i % 4) == 3) {
|
|
*buf++ = val;
|
|
val = pad;
|
|
num--;
|
|
}
|
|
}
|
|
if (--num >= 0)
|
|
*buf++ = val;
|
|
while (--num >= 0)
|
|
*buf++ = pad;
|
|
}
|
|
|
|
/*
|
|
* Returns the hash of a filename. If len is 0 and name is NULL, then
|
|
* this function can be used to test whether or not a hash version is
|
|
* supported.
|
|
*
|
|
* The seed is an 4 longword (32 bits) "secret" which can be used to
|
|
* uniquify a hash. If the seed is all zero's, then some default seed
|
|
* may be used.
|
|
*
|
|
* A particular hash version specifies whether or not the seed is
|
|
* represented, and whether or not the returned hash is 32 bits or 64
|
|
* bits. 32 bit hashes will return 0 for the minor hash.
|
|
*/
|
|
static int __ext4fs_dirhash(const char *name, int len,
|
|
struct dx_hash_info *hinfo)
|
|
{
|
|
__u32 hash;
|
|
__u32 minor_hash = 0;
|
|
const char *p;
|
|
int i;
|
|
__u32 in[8], buf[4];
|
|
void (*str2hashbuf)(const char *, int, __u32 *, int) =
|
|
str2hashbuf_signed;
|
|
|
|
/* Initialize the default seed for the hash checksum functions */
|
|
buf[0] = 0x67452301;
|
|
buf[1] = 0xefcdab89;
|
|
buf[2] = 0x98badcfe;
|
|
buf[3] = 0x10325476;
|
|
|
|
/* Check to see if the seed is all zero's */
|
|
if (hinfo->seed) {
|
|
for (i = 0; i < 4; i++) {
|
|
if (hinfo->seed[i]) {
|
|
memcpy(buf, hinfo->seed, sizeof(buf));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (hinfo->hash_version) {
|
|
case DX_HASH_LEGACY_UNSIGNED:
|
|
hash = dx_hack_hash_unsigned(name, len);
|
|
break;
|
|
case DX_HASH_LEGACY:
|
|
hash = dx_hack_hash_signed(name, len);
|
|
break;
|
|
case DX_HASH_HALF_MD4_UNSIGNED:
|
|
str2hashbuf = str2hashbuf_unsigned;
|
|
/* fall through */
|
|
case DX_HASH_HALF_MD4:
|
|
p = name;
|
|
while (len > 0) {
|
|
(*str2hashbuf)(p, len, in, 8);
|
|
half_md4_transform(buf, in);
|
|
len -= 32;
|
|
p += 32;
|
|
}
|
|
minor_hash = buf[2];
|
|
hash = buf[1];
|
|
break;
|
|
case DX_HASH_TEA_UNSIGNED:
|
|
str2hashbuf = str2hashbuf_unsigned;
|
|
/* fall through */
|
|
case DX_HASH_TEA:
|
|
p = name;
|
|
while (len > 0) {
|
|
(*str2hashbuf)(p, len, in, 4);
|
|
TEA_transform(buf, in);
|
|
len -= 16;
|
|
p += 16;
|
|
}
|
|
hash = buf[0];
|
|
minor_hash = buf[1];
|
|
break;
|
|
default:
|
|
hinfo->hash = 0;
|
|
return -1;
|
|
}
|
|
hash = hash & ~1;
|
|
if (hash == (EXT4_HTREE_EOF_32BIT << 1))
|
|
hash = (EXT4_HTREE_EOF_32BIT - 1) << 1;
|
|
hinfo->hash = hash;
|
|
hinfo->minor_hash = minor_hash;
|
|
return 0;
|
|
}
|
|
|
|
int ext4fs_dirhash(const struct inode *dir, const char *name, int len,
|
|
struct dx_hash_info *hinfo)
|
|
{
|
|
#ifdef CONFIG_UNICODE
|
|
const struct unicode_map *um = EXT4_SB(dir->i_sb)->s_encoding;
|
|
int r, dlen;
|
|
unsigned char *buff;
|
|
struct qstr qstr = {.name = name, .len = len };
|
|
|
|
if (len && IS_CASEFOLDED(dir)) {
|
|
buff = kzalloc(sizeof(char) * PATH_MAX, GFP_KERNEL);
|
|
if (!buff)
|
|
return -ENOMEM;
|
|
|
|
dlen = utf8_casefold(um, &qstr, buff, PATH_MAX);
|
|
if (dlen < 0) {
|
|
kfree(buff);
|
|
goto opaque_seq;
|
|
}
|
|
|
|
r = __ext4fs_dirhash(buff, dlen, hinfo);
|
|
|
|
kfree(buff);
|
|
return r;
|
|
}
|
|
opaque_seq:
|
|
#endif
|
|
return __ext4fs_dirhash(name, len, hinfo);
|
|
}
|