summaryrefslogtreecommitdiffstats
path: root/fs/fat/namei_msdos.c
diff options
context:
space:
mode:
Diffstat (limited to 'fs/fat/namei_msdos.c')
-rw-r--r--fs/fat/namei_msdos.c689
1 files changed, 689 insertions, 0 deletions
diff --git a/fs/fat/namei_msdos.c b/fs/fat/namei_msdos.c
new file mode 100644
index 000000000..efba301d6
--- /dev/null
+++ b/fs/fat/namei_msdos.c
@@ -0,0 +1,689 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * linux/fs/msdos/namei.c
+ *
+ * Written 1992,1993 by Werner Almesberger
+ * Hidden files 1995 by Albert Cahalan <albert@ccs.neu.edu> <adc@coe.neu.edu>
+ * Rewritten for constant inumbers 1999 by Al Viro
+ */
+
+#include <linux/module.h>
+#include <linux/iversion.h>
+#include "fat.h"
+
+/* Characters that are undesirable in an MS-DOS file name */
+static unsigned char bad_chars[] = "*?<>|\"";
+static unsigned char bad_if_strict[] = "+=,; ";
+
+/***** Formats an MS-DOS file name. Rejects invalid names. */
+static int msdos_format_name(const unsigned char *name, int len,
+ unsigned char *res, struct fat_mount_options *opts)
+ /*
+ * name is the proposed name, len is its length, res is
+ * the resulting name, opts->name_check is either (r)elaxed,
+ * (n)ormal or (s)trict, opts->dotsOK allows dots at the
+ * beginning of name (for hidden files)
+ */
+{
+ unsigned char *walk;
+ unsigned char c;
+ int space;
+
+ if (name[0] == '.') { /* dotfile because . and .. already done */
+ if (opts->dotsOK) {
+ /* Get rid of dot - test for it elsewhere */
+ name++;
+ len--;
+ } else
+ return -EINVAL;
+ }
+ /*
+ * disallow names that _really_ start with a dot
+ */
+ space = 1;
+ c = 0;
+ for (walk = res; len && walk - res < 8; walk++) {
+ c = *name++;
+ len--;
+ if (opts->name_check != 'r' && strchr(bad_chars, c))
+ return -EINVAL;
+ if (opts->name_check == 's' && strchr(bad_if_strict, c))
+ return -EINVAL;
+ if (c >= 'A' && c <= 'Z' && opts->name_check == 's')
+ return -EINVAL;
+ if (c < ' ' || c == ':' || c == '\\')
+ return -EINVAL;
+ /*
+ * 0xE5 is legal as a first character, but we must substitute
+ * 0x05 because 0xE5 marks deleted files. Yes, DOS really
+ * does this.
+ * It seems that Microsoft hacked DOS to support non-US
+ * characters after the 0xE5 character was already in use to
+ * mark deleted files.
+ */
+ if ((res == walk) && (c == 0xE5))
+ c = 0x05;
+ if (c == '.')
+ break;
+ space = (c == ' ');
+ *walk = (!opts->nocase && c >= 'a' && c <= 'z') ? c - 32 : c;
+ }
+ if (space)
+ return -EINVAL;
+ if (opts->name_check == 's' && len && c != '.') {
+ c = *name++;
+ len--;
+ if (c != '.')
+ return -EINVAL;
+ }
+ while (c != '.' && len--)
+ c = *name++;
+ if (c == '.') {
+ while (walk - res < 8)
+ *walk++ = ' ';
+ while (len > 0 && walk - res < MSDOS_NAME) {
+ c = *name++;
+ len--;
+ if (opts->name_check != 'r' && strchr(bad_chars, c))
+ return -EINVAL;
+ if (opts->name_check == 's' &&
+ strchr(bad_if_strict, c))
+ return -EINVAL;
+ if (c < ' ' || c == ':' || c == '\\')
+ return -EINVAL;
+ if (c == '.') {
+ if (opts->name_check == 's')
+ return -EINVAL;
+ break;
+ }
+ if (c >= 'A' && c <= 'Z' && opts->name_check == 's')
+ return -EINVAL;
+ space = c == ' ';
+ if (!opts->nocase && c >= 'a' && c <= 'z')
+ *walk++ = c - 32;
+ else
+ *walk++ = c;
+ }
+ if (space)
+ return -EINVAL;
+ if (opts->name_check == 's' && len)
+ return -EINVAL;
+ }
+ while (walk - res < MSDOS_NAME)
+ *walk++ = ' ';
+
+ return 0;
+}
+
+/***** Locates a directory entry. Uses unformatted name. */
+static int msdos_find(struct inode *dir, const unsigned char *name, int len,
+ struct fat_slot_info *sinfo)
+{
+ struct msdos_sb_info *sbi = MSDOS_SB(dir->i_sb);
+ unsigned char msdos_name[MSDOS_NAME];
+ int err;
+
+ err = msdos_format_name(name, len, msdos_name, &sbi->options);
+ if (err)
+ return -ENOENT;
+
+ err = fat_scan(dir, msdos_name, sinfo);
+ if (!err && sbi->options.dotsOK) {
+ if (name[0] == '.') {
+ if (!(sinfo->de->attr & ATTR_HIDDEN))
+ err = -ENOENT;
+ } else {
+ if (sinfo->de->attr & ATTR_HIDDEN)
+ err = -ENOENT;
+ }
+ if (err)
+ brelse(sinfo->bh);
+ }
+ return err;
+}
+
+/*
+ * Compute the hash for the msdos name corresponding to the dentry.
+ * Note: if the name is invalid, we leave the hash code unchanged so
+ * that the existing dentry can be used. The msdos fs routines will
+ * return ENOENT or EINVAL as appropriate.
+ */
+static int msdos_hash(const struct dentry *dentry, struct qstr *qstr)
+{
+ struct fat_mount_options *options = &MSDOS_SB(dentry->d_sb)->options;
+ unsigned char msdos_name[MSDOS_NAME];
+ int error;
+
+ error = msdos_format_name(qstr->name, qstr->len, msdos_name, options);
+ if (!error)
+ qstr->hash = full_name_hash(dentry, msdos_name, MSDOS_NAME);
+ return 0;
+}
+
+/*
+ * Compare two msdos names. If either of the names are invalid,
+ * we fall back to doing the standard name comparison.
+ */
+static int msdos_cmp(const struct dentry *dentry,
+ unsigned int len, const char *str, const struct qstr *name)
+{
+ struct fat_mount_options *options = &MSDOS_SB(dentry->d_sb)->options;
+ unsigned char a_msdos_name[MSDOS_NAME], b_msdos_name[MSDOS_NAME];
+ int error;
+
+ error = msdos_format_name(name->name, name->len, a_msdos_name, options);
+ if (error)
+ goto old_compare;
+ error = msdos_format_name(str, len, b_msdos_name, options);
+ if (error)
+ goto old_compare;
+ error = memcmp(a_msdos_name, b_msdos_name, MSDOS_NAME);
+out:
+ return error;
+
+old_compare:
+ error = 1;
+ if (name->len == len)
+ error = memcmp(name->name, str, len);
+ goto out;
+}
+
+static const struct dentry_operations msdos_dentry_operations = {
+ .d_hash = msdos_hash,
+ .d_compare = msdos_cmp,
+};
+
+/*
+ * AV. Wrappers for FAT sb operations. Is it wise?
+ */
+
+/***** Get inode using directory and name */
+static struct dentry *msdos_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags)
+{
+ struct super_block *sb = dir->i_sb;
+ struct fat_slot_info sinfo;
+ struct inode *inode;
+ int err;
+
+ mutex_lock(&MSDOS_SB(sb)->s_lock);
+ err = msdos_find(dir, dentry->d_name.name, dentry->d_name.len, &sinfo);
+ switch (err) {
+ case -ENOENT:
+ inode = NULL;
+ break;
+ case 0:
+ inode = fat_build_inode(sb, sinfo.de, sinfo.i_pos);
+ brelse(sinfo.bh);
+ break;
+ default:
+ inode = ERR_PTR(err);
+ }
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ return d_splice_alias(inode, dentry);
+}
+
+/***** Creates a directory entry (name is already formatted). */
+static int msdos_add_entry(struct inode *dir, const unsigned char *name,
+ int is_dir, int is_hid, int cluster,
+ struct timespec64 *ts, struct fat_slot_info *sinfo)
+{
+ struct msdos_sb_info *sbi = MSDOS_SB(dir->i_sb);
+ struct msdos_dir_entry de;
+ __le16 time, date;
+ int err;
+
+ memcpy(de.name, name, MSDOS_NAME);
+ de.attr = is_dir ? ATTR_DIR : ATTR_ARCH;
+ if (is_hid)
+ de.attr |= ATTR_HIDDEN;
+ de.lcase = 0;
+ fat_time_unix2fat(sbi, ts, &time, &date, NULL);
+ de.cdate = de.adate = 0;
+ de.ctime = 0;
+ de.ctime_cs = 0;
+ de.time = time;
+ de.date = date;
+ fat_set_start(&de, cluster);
+ de.size = 0;
+
+ err = fat_add_entries(dir, &de, 1, sinfo);
+ if (err)
+ return err;
+
+ fat_truncate_time(dir, ts, S_CTIME|S_MTIME);
+ if (IS_DIRSYNC(dir))
+ (void)fat_sync_inode(dir);
+ else
+ mark_inode_dirty(dir);
+
+ return 0;
+}
+
+/***** Create a file */
+static int msdos_create(struct user_namespace *mnt_userns, struct inode *dir,
+ struct dentry *dentry, umode_t mode, bool excl)
+{
+ struct super_block *sb = dir->i_sb;
+ struct inode *inode = NULL;
+ struct fat_slot_info sinfo;
+ struct timespec64 ts;
+ unsigned char msdos_name[MSDOS_NAME];
+ int err, is_hid;
+
+ mutex_lock(&MSDOS_SB(sb)->s_lock);
+
+ err = msdos_format_name(dentry->d_name.name, dentry->d_name.len,
+ msdos_name, &MSDOS_SB(sb)->options);
+ if (err)
+ goto out;
+ is_hid = (dentry->d_name.name[0] == '.') && (msdos_name[0] != '.');
+ /* Have to do it due to foo vs. .foo conflicts */
+ if (!fat_scan(dir, msdos_name, &sinfo)) {
+ brelse(sinfo.bh);
+ err = -EINVAL;
+ goto out;
+ }
+
+ ts = current_time(dir);
+ err = msdos_add_entry(dir, msdos_name, 0, is_hid, 0, &ts, &sinfo);
+ if (err)
+ goto out;
+ inode = fat_build_inode(sb, sinfo.de, sinfo.i_pos);
+ brelse(sinfo.bh);
+ if (IS_ERR(inode)) {
+ err = PTR_ERR(inode);
+ goto out;
+ }
+ fat_truncate_time(inode, &ts, S_ATIME|S_CTIME|S_MTIME);
+ /* timestamp is already written, so mark_inode_dirty() is unneeded. */
+
+ d_instantiate(dentry, inode);
+out:
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ if (!err)
+ err = fat_flush_inodes(sb, dir, inode);
+ return err;
+}
+
+/***** Remove a directory */
+static int msdos_rmdir(struct inode *dir, struct dentry *dentry)
+{
+ struct super_block *sb = dir->i_sb;
+ struct inode *inode = d_inode(dentry);
+ struct fat_slot_info sinfo;
+ int err;
+
+ mutex_lock(&MSDOS_SB(sb)->s_lock);
+ err = fat_dir_empty(inode);
+ if (err)
+ goto out;
+ err = msdos_find(dir, dentry->d_name.name, dentry->d_name.len, &sinfo);
+ if (err)
+ goto out;
+
+ err = fat_remove_entries(dir, &sinfo); /* and releases bh */
+ if (err)
+ goto out;
+ drop_nlink(dir);
+
+ clear_nlink(inode);
+ fat_truncate_time(inode, NULL, S_CTIME);
+ fat_detach(inode);
+out:
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ if (!err)
+ err = fat_flush_inodes(sb, dir, inode);
+
+ return err;
+}
+
+/***** Make a directory */
+static int msdos_mkdir(struct user_namespace *mnt_userns, struct inode *dir,
+ struct dentry *dentry, umode_t mode)
+{
+ struct super_block *sb = dir->i_sb;
+ struct fat_slot_info sinfo;
+ struct inode *inode;
+ unsigned char msdos_name[MSDOS_NAME];
+ struct timespec64 ts;
+ int err, is_hid, cluster;
+
+ mutex_lock(&MSDOS_SB(sb)->s_lock);
+
+ err = msdos_format_name(dentry->d_name.name, dentry->d_name.len,
+ msdos_name, &MSDOS_SB(sb)->options);
+ if (err)
+ goto out;
+ is_hid = (dentry->d_name.name[0] == '.') && (msdos_name[0] != '.');
+ /* foo vs .foo situation */
+ if (!fat_scan(dir, msdos_name, &sinfo)) {
+ brelse(sinfo.bh);
+ err = -EINVAL;
+ goto out;
+ }
+
+ ts = current_time(dir);
+ cluster = fat_alloc_new_dir(dir, &ts);
+ if (cluster < 0) {
+ err = cluster;
+ goto out;
+ }
+ err = msdos_add_entry(dir, msdos_name, 1, is_hid, cluster, &ts, &sinfo);
+ if (err)
+ goto out_free;
+ inc_nlink(dir);
+
+ inode = fat_build_inode(sb, sinfo.de, sinfo.i_pos);
+ brelse(sinfo.bh);
+ if (IS_ERR(inode)) {
+ err = PTR_ERR(inode);
+ /* the directory was completed, just return a error */
+ goto out;
+ }
+ set_nlink(inode, 2);
+ fat_truncate_time(inode, &ts, S_ATIME|S_CTIME|S_MTIME);
+ /* timestamp is already written, so mark_inode_dirty() is unneeded. */
+
+ d_instantiate(dentry, inode);
+
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ fat_flush_inodes(sb, dir, inode);
+ return 0;
+
+out_free:
+ fat_free_clusters(dir, cluster);
+out:
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ return err;
+}
+
+/***** Unlink a file */
+static int msdos_unlink(struct inode *dir, struct dentry *dentry)
+{
+ struct inode *inode = d_inode(dentry);
+ struct super_block *sb = inode->i_sb;
+ struct fat_slot_info sinfo;
+ int err;
+
+ mutex_lock(&MSDOS_SB(sb)->s_lock);
+ err = msdos_find(dir, dentry->d_name.name, dentry->d_name.len, &sinfo);
+ if (err)
+ goto out;
+
+ err = fat_remove_entries(dir, &sinfo); /* and releases bh */
+ if (err)
+ goto out;
+ clear_nlink(inode);
+ fat_truncate_time(inode, NULL, S_CTIME);
+ fat_detach(inode);
+out:
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ if (!err)
+ err = fat_flush_inodes(sb, dir, inode);
+
+ return err;
+}
+
+static int do_msdos_rename(struct inode *old_dir, unsigned char *old_name,
+ struct dentry *old_dentry,
+ struct inode *new_dir, unsigned char *new_name,
+ struct dentry *new_dentry, int is_hid)
+{
+ struct buffer_head *dotdot_bh;
+ struct msdos_dir_entry *dotdot_de;
+ struct inode *old_inode, *new_inode;
+ struct fat_slot_info old_sinfo, sinfo;
+ struct timespec64 ts;
+ loff_t new_i_pos;
+ int err, old_attrs, is_dir, update_dotdot, corrupt = 0;
+
+ old_sinfo.bh = sinfo.bh = dotdot_bh = NULL;
+ old_inode = d_inode(old_dentry);
+ new_inode = d_inode(new_dentry);
+
+ err = fat_scan(old_dir, old_name, &old_sinfo);
+ if (err) {
+ err = -EIO;
+ goto out;
+ }
+
+ is_dir = S_ISDIR(old_inode->i_mode);
+ update_dotdot = (is_dir && old_dir != new_dir);
+ if (update_dotdot) {
+ if (fat_get_dotdot_entry(old_inode, &dotdot_bh, &dotdot_de)) {
+ err = -EIO;
+ goto out;
+ }
+ }
+
+ old_attrs = MSDOS_I(old_inode)->i_attrs;
+ err = fat_scan(new_dir, new_name, &sinfo);
+ if (!err) {
+ if (!new_inode) {
+ /* "foo" -> ".foo" case. just change the ATTR_HIDDEN */
+ if (sinfo.de != old_sinfo.de) {
+ err = -EINVAL;
+ goto out;
+ }
+ if (is_hid)
+ MSDOS_I(old_inode)->i_attrs |= ATTR_HIDDEN;
+ else
+ MSDOS_I(old_inode)->i_attrs &= ~ATTR_HIDDEN;
+ if (IS_DIRSYNC(old_dir)) {
+ err = fat_sync_inode(old_inode);
+ if (err) {
+ MSDOS_I(old_inode)->i_attrs = old_attrs;
+ goto out;
+ }
+ } else
+ mark_inode_dirty(old_inode);
+
+ inode_inc_iversion(old_dir);
+ fat_truncate_time(old_dir, NULL, S_CTIME|S_MTIME);
+ if (IS_DIRSYNC(old_dir))
+ (void)fat_sync_inode(old_dir);
+ else
+ mark_inode_dirty(old_dir);
+ goto out;
+ }
+ }
+
+ ts = current_time(old_inode);
+ if (new_inode) {
+ if (err)
+ goto out;
+ if (is_dir) {
+ err = fat_dir_empty(new_inode);
+ if (err)
+ goto out;
+ }
+ new_i_pos = MSDOS_I(new_inode)->i_pos;
+ fat_detach(new_inode);
+ } else {
+ err = msdos_add_entry(new_dir, new_name, is_dir, is_hid, 0,
+ &ts, &sinfo);
+ if (err)
+ goto out;
+ new_i_pos = sinfo.i_pos;
+ }
+ inode_inc_iversion(new_dir);
+
+ fat_detach(old_inode);
+ fat_attach(old_inode, new_i_pos);
+ if (is_hid)
+ MSDOS_I(old_inode)->i_attrs |= ATTR_HIDDEN;
+ else
+ MSDOS_I(old_inode)->i_attrs &= ~ATTR_HIDDEN;
+ if (IS_DIRSYNC(new_dir)) {
+ err = fat_sync_inode(old_inode);
+ if (err)
+ goto error_inode;
+ } else
+ mark_inode_dirty(old_inode);
+
+ if (update_dotdot) {
+ fat_set_start(dotdot_de, MSDOS_I(new_dir)->i_logstart);
+ mark_buffer_dirty_inode(dotdot_bh, old_inode);
+ if (IS_DIRSYNC(new_dir)) {
+ err = sync_dirty_buffer(dotdot_bh);
+ if (err)
+ goto error_dotdot;
+ }
+ drop_nlink(old_dir);
+ if (!new_inode)
+ inc_nlink(new_dir);
+ }
+
+ err = fat_remove_entries(old_dir, &old_sinfo); /* and releases bh */
+ old_sinfo.bh = NULL;
+ if (err)
+ goto error_dotdot;
+ inode_inc_iversion(old_dir);
+ fat_truncate_time(old_dir, &ts, S_CTIME|S_MTIME);
+ if (IS_DIRSYNC(old_dir))
+ (void)fat_sync_inode(old_dir);
+ else
+ mark_inode_dirty(old_dir);
+
+ if (new_inode) {
+ drop_nlink(new_inode);
+ if (is_dir)
+ drop_nlink(new_inode);
+ fat_truncate_time(new_inode, &ts, S_CTIME);
+ }
+out:
+ brelse(sinfo.bh);
+ brelse(dotdot_bh);
+ brelse(old_sinfo.bh);
+ return err;
+
+error_dotdot:
+ /* data cluster is shared, serious corruption */
+ corrupt = 1;
+
+ if (update_dotdot) {
+ fat_set_start(dotdot_de, MSDOS_I(old_dir)->i_logstart);
+ mark_buffer_dirty_inode(dotdot_bh, old_inode);
+ corrupt |= sync_dirty_buffer(dotdot_bh);
+ }
+error_inode:
+ fat_detach(old_inode);
+ fat_attach(old_inode, old_sinfo.i_pos);
+ MSDOS_I(old_inode)->i_attrs = old_attrs;
+ if (new_inode) {
+ fat_attach(new_inode, new_i_pos);
+ if (corrupt)
+ corrupt |= fat_sync_inode(new_inode);
+ } else {
+ /*
+ * If new entry was not sharing the data cluster, it
+ * shouldn't be serious corruption.
+ */
+ int err2 = fat_remove_entries(new_dir, &sinfo);
+ if (corrupt)
+ corrupt |= err2;
+ sinfo.bh = NULL;
+ }
+ if (corrupt < 0) {
+ fat_fs_error(new_dir->i_sb,
+ "%s: Filesystem corrupted (i_pos %lld)",
+ __func__, sinfo.i_pos);
+ }
+ goto out;
+}
+
+/***** Rename, a wrapper for rename_same_dir & rename_diff_dir */
+static int msdos_rename(struct user_namespace *mnt_userns,
+ struct inode *old_dir, struct dentry *old_dentry,
+ struct inode *new_dir, struct dentry *new_dentry,
+ unsigned int flags)
+{
+ struct super_block *sb = old_dir->i_sb;
+ unsigned char old_msdos_name[MSDOS_NAME], new_msdos_name[MSDOS_NAME];
+ int err, is_hid;
+
+ if (flags & ~RENAME_NOREPLACE)
+ return -EINVAL;
+
+ mutex_lock(&MSDOS_SB(sb)->s_lock);
+
+ err = msdos_format_name(old_dentry->d_name.name,
+ old_dentry->d_name.len, old_msdos_name,
+ &MSDOS_SB(old_dir->i_sb)->options);
+ if (err)
+ goto out;
+ err = msdos_format_name(new_dentry->d_name.name,
+ new_dentry->d_name.len, new_msdos_name,
+ &MSDOS_SB(new_dir->i_sb)->options);
+ if (err)
+ goto out;
+
+ is_hid =
+ (new_dentry->d_name.name[0] == '.') && (new_msdos_name[0] != '.');
+
+ err = do_msdos_rename(old_dir, old_msdos_name, old_dentry,
+ new_dir, new_msdos_name, new_dentry, is_hid);
+out:
+ mutex_unlock(&MSDOS_SB(sb)->s_lock);
+ if (!err)
+ err = fat_flush_inodes(sb, old_dir, new_dir);
+ return err;
+}
+
+static const struct inode_operations msdos_dir_inode_operations = {
+ .create = msdos_create,
+ .lookup = msdos_lookup,
+ .unlink = msdos_unlink,
+ .mkdir = msdos_mkdir,
+ .rmdir = msdos_rmdir,
+ .rename = msdos_rename,
+ .setattr = fat_setattr,
+ .getattr = fat_getattr,
+ .update_time = fat_update_time,
+};
+
+static void setup(struct super_block *sb)
+{
+ MSDOS_SB(sb)->dir_ops = &msdos_dir_inode_operations;
+ sb->s_d_op = &msdos_dentry_operations;
+ sb->s_flags |= SB_NOATIME;
+}
+
+static int msdos_fill_super(struct super_block *sb, void *data, int silent)
+{
+ return fat_fill_super(sb, data, silent, 0, setup);
+}
+
+static struct dentry *msdos_mount(struct file_system_type *fs_type,
+ int flags, const char *dev_name,
+ void *data)
+{
+ return mount_bdev(fs_type, flags, dev_name, data, msdos_fill_super);
+}
+
+static struct file_system_type msdos_fs_type = {
+ .owner = THIS_MODULE,
+ .name = "msdos",
+ .mount = msdos_mount,
+ .kill_sb = kill_block_super,
+ .fs_flags = FS_REQUIRES_DEV | FS_ALLOW_IDMAP,
+};
+MODULE_ALIAS_FS("msdos");
+
+static int __init init_msdos_fs(void)
+{
+ return register_filesystem(&msdos_fs_type);
+}
+
+static void __exit exit_msdos_fs(void)
+{
+ unregister_filesystem(&msdos_fs_type);
+}
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Werner Almesberger");
+MODULE_DESCRIPTION("MS-DOS filesystem support");
+
+module_init(init_msdos_fs)
+module_exit(exit_msdos_fs)