|  | // SPDX-License-Identifier: GPL-2.0-only | 
|  | /* | 
|  | * OMFS (as used by RIO Karma) directory operations. | 
|  | * Copyright (C) 2005 Bob Copeland <me@bobcopeland.com> | 
|  | */ | 
|  |  | 
|  | #include <linux/fs.h> | 
|  | #include <linux/ctype.h> | 
|  | #include <linux/buffer_head.h> | 
|  | #include "omfs.h" | 
|  |  | 
|  | static int omfs_hash(const char *name, int namelen, int mod) | 
|  | { | 
|  | int i, hash = 0; | 
|  | for (i = 0; i < namelen; i++) | 
|  | hash ^= tolower(name[i]) << (i % 24); | 
|  | return hash % mod; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Finds the bucket for a given name and reads the containing block; | 
|  | * *ofs is set to the offset of the first list entry. | 
|  | */ | 
|  | static struct buffer_head *omfs_get_bucket(struct inode *dir, | 
|  | const char *name, int namelen, int *ofs) | 
|  | { | 
|  | int nbuckets = (dir->i_size - OMFS_DIR_START)/8; | 
|  | int bucket = omfs_hash(name, namelen, nbuckets); | 
|  |  | 
|  | *ofs = OMFS_DIR_START + bucket * 8; | 
|  | return omfs_bread(dir->i_sb, dir->i_ino); | 
|  | } | 
|  |  | 
|  | static struct buffer_head *omfs_scan_list(struct inode *dir, u64 block, | 
|  | const char *name, int namelen, | 
|  | u64 *prev_block) | 
|  | { | 
|  | struct buffer_head *bh; | 
|  | struct omfs_inode *oi; | 
|  | int err = -ENOENT; | 
|  | *prev_block = ~0; | 
|  |  | 
|  | while (block != ~0) { | 
|  | bh = omfs_bread(dir->i_sb, block); | 
|  | if (!bh) { | 
|  | err = -EIO; | 
|  | goto err; | 
|  | } | 
|  |  | 
|  | oi = (struct omfs_inode *) bh->b_data; | 
|  | if (omfs_is_bad(OMFS_SB(dir->i_sb), &oi->i_head, block)) { | 
|  | brelse(bh); | 
|  | goto err; | 
|  | } | 
|  |  | 
|  | if (strncmp(oi->i_name, name, namelen) == 0) | 
|  | return bh; | 
|  |  | 
|  | *prev_block = block; | 
|  | block = be64_to_cpu(oi->i_sibling); | 
|  | brelse(bh); | 
|  | } | 
|  | err: | 
|  | return ERR_PTR(err); | 
|  | } | 
|  |  | 
|  | static struct buffer_head *omfs_find_entry(struct inode *dir, | 
|  | const char *name, int namelen) | 
|  | { | 
|  | struct buffer_head *bh; | 
|  | int ofs; | 
|  | u64 block, dummy; | 
|  |  | 
|  | bh = omfs_get_bucket(dir, name, namelen, &ofs); | 
|  | if (!bh) | 
|  | return ERR_PTR(-EIO); | 
|  |  | 
|  | block = be64_to_cpu(*((__be64 *) &bh->b_data[ofs])); | 
|  | brelse(bh); | 
|  |  | 
|  | return omfs_scan_list(dir, block, name, namelen, &dummy); | 
|  | } | 
|  |  | 
|  | int omfs_make_empty(struct inode *inode, struct super_block *sb) | 
|  | { | 
|  | struct omfs_sb_info *sbi = OMFS_SB(sb); | 
|  | struct buffer_head *bh; | 
|  | struct omfs_inode *oi; | 
|  |  | 
|  | bh = omfs_bread(sb, inode->i_ino); | 
|  | if (!bh) | 
|  | return -ENOMEM; | 
|  |  | 
|  | memset(bh->b_data, 0, sizeof(struct omfs_inode)); | 
|  |  | 
|  | if (S_ISDIR(inode->i_mode)) { | 
|  | memset(&bh->b_data[OMFS_DIR_START], 0xff, | 
|  | sbi->s_sys_blocksize - OMFS_DIR_START); | 
|  | } else | 
|  | omfs_make_empty_table(bh, OMFS_EXTENT_START); | 
|  |  | 
|  | oi = (struct omfs_inode *) bh->b_data; | 
|  | oi->i_head.h_self = cpu_to_be64(inode->i_ino); | 
|  | oi->i_sibling = ~cpu_to_be64(0ULL); | 
|  |  | 
|  | mark_buffer_dirty(bh); | 
|  | brelse(bh); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int omfs_add_link(struct dentry *dentry, struct inode *inode) | 
|  | { | 
|  | struct inode *dir = d_inode(dentry->d_parent); | 
|  | const char *name = dentry->d_name.name; | 
|  | int namelen = dentry->d_name.len; | 
|  | struct omfs_inode *oi; | 
|  | struct buffer_head *bh; | 
|  | u64 block; | 
|  | __be64 *entry; | 
|  | int ofs; | 
|  |  | 
|  | /* just prepend to head of queue in proper bucket */ | 
|  | bh = omfs_get_bucket(dir, name, namelen, &ofs); | 
|  | if (!bh) | 
|  | goto out; | 
|  |  | 
|  | entry = (__be64 *) &bh->b_data[ofs]; | 
|  | block = be64_to_cpu(*entry); | 
|  | *entry = cpu_to_be64(inode->i_ino); | 
|  | mark_buffer_dirty(bh); | 
|  | brelse(bh); | 
|  |  | 
|  | /* now set the sibling and parent pointers on the new inode */ | 
|  | bh = omfs_bread(dir->i_sb, inode->i_ino); | 
|  | if (!bh) | 
|  | goto out; | 
|  |  | 
|  | oi = (struct omfs_inode *) bh->b_data; | 
|  | memcpy(oi->i_name, name, namelen); | 
|  | memset(oi->i_name + namelen, 0, OMFS_NAMELEN - namelen); | 
|  | oi->i_sibling = cpu_to_be64(block); | 
|  | oi->i_parent = cpu_to_be64(dir->i_ino); | 
|  | mark_buffer_dirty(bh); | 
|  | brelse(bh); | 
|  |  | 
|  | inode_set_ctime_current(dir); | 
|  |  | 
|  | /* mark affected inodes dirty to rebuild checksums */ | 
|  | mark_inode_dirty(dir); | 
|  | mark_inode_dirty(inode); | 
|  | return 0; | 
|  | out: | 
|  | return -ENOMEM; | 
|  | } | 
|  |  | 
|  | static int omfs_delete_entry(struct dentry *dentry) | 
|  | { | 
|  | struct inode *dir = d_inode(dentry->d_parent); | 
|  | struct inode *dirty; | 
|  | const char *name = dentry->d_name.name; | 
|  | int namelen = dentry->d_name.len; | 
|  | struct omfs_inode *oi; | 
|  | struct buffer_head *bh, *bh2; | 
|  | __be64 *entry, next; | 
|  | u64 block, prev; | 
|  | int ofs; | 
|  | int err = -ENOMEM; | 
|  |  | 
|  | /* delete the proper node in the bucket's linked list */ | 
|  | bh = omfs_get_bucket(dir, name, namelen, &ofs); | 
|  | if (!bh) | 
|  | goto out; | 
|  |  | 
|  | entry = (__be64 *) &bh->b_data[ofs]; | 
|  | block = be64_to_cpu(*entry); | 
|  |  | 
|  | bh2 = omfs_scan_list(dir, block, name, namelen, &prev); | 
|  | if (IS_ERR(bh2)) { | 
|  | err = PTR_ERR(bh2); | 
|  | goto out_free_bh; | 
|  | } | 
|  |  | 
|  | oi = (struct omfs_inode *) bh2->b_data; | 
|  | next = oi->i_sibling; | 
|  | brelse(bh2); | 
|  |  | 
|  | if (prev != ~0) { | 
|  | /* found in middle of list, get list ptr */ | 
|  | brelse(bh); | 
|  | bh = omfs_bread(dir->i_sb, prev); | 
|  | if (!bh) | 
|  | goto out; | 
|  |  | 
|  | oi = (struct omfs_inode *) bh->b_data; | 
|  | entry = &oi->i_sibling; | 
|  | } | 
|  |  | 
|  | *entry = next; | 
|  | mark_buffer_dirty(bh); | 
|  |  | 
|  | if (prev != ~0) { | 
|  | dirty = omfs_iget(dir->i_sb, prev); | 
|  | if (!IS_ERR(dirty)) { | 
|  | mark_inode_dirty(dirty); | 
|  | iput(dirty); | 
|  | } | 
|  | } | 
|  |  | 
|  | err = 0; | 
|  | out_free_bh: | 
|  | brelse(bh); | 
|  | out: | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static int omfs_dir_is_empty(struct inode *inode) | 
|  | { | 
|  | int nbuckets = (inode->i_size - OMFS_DIR_START) / 8; | 
|  | struct buffer_head *bh; | 
|  | u64 *ptr; | 
|  | int i; | 
|  |  | 
|  | bh = omfs_bread(inode->i_sb, inode->i_ino); | 
|  |  | 
|  | if (!bh) | 
|  | return 0; | 
|  |  | 
|  | ptr = (u64 *) &bh->b_data[OMFS_DIR_START]; | 
|  |  | 
|  | for (i = 0; i < nbuckets; i++, ptr++) | 
|  | if (*ptr != ~0) | 
|  | break; | 
|  |  | 
|  | brelse(bh); | 
|  | return *ptr != ~0; | 
|  | } | 
|  |  | 
|  | static int omfs_remove(struct inode *dir, struct dentry *dentry) | 
|  | { | 
|  | struct inode *inode = d_inode(dentry); | 
|  | int ret; | 
|  |  | 
|  |  | 
|  | if (S_ISDIR(inode->i_mode) && | 
|  | !omfs_dir_is_empty(inode)) | 
|  | return -ENOTEMPTY; | 
|  |  | 
|  | ret = omfs_delete_entry(dentry); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | clear_nlink(inode); | 
|  | mark_inode_dirty(inode); | 
|  | mark_inode_dirty(dir); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int omfs_add_node(struct inode *dir, struct dentry *dentry, umode_t mode) | 
|  | { | 
|  | int err; | 
|  | struct inode *inode = omfs_new_inode(dir, mode); | 
|  |  | 
|  | if (IS_ERR(inode)) | 
|  | return PTR_ERR(inode); | 
|  |  | 
|  | err = omfs_make_empty(inode, dir->i_sb); | 
|  | if (err) | 
|  | goto out_free_inode; | 
|  |  | 
|  | err = omfs_add_link(dentry, inode); | 
|  | if (err) | 
|  | goto out_free_inode; | 
|  |  | 
|  | d_instantiate(dentry, inode); | 
|  | return 0; | 
|  |  | 
|  | out_free_inode: | 
|  | iput(inode); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static int omfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, | 
|  | struct dentry *dentry, umode_t mode) | 
|  | { | 
|  | return omfs_add_node(dir, dentry, mode | S_IFDIR); | 
|  | } | 
|  |  | 
|  | static int omfs_create(struct mnt_idmap *idmap, struct inode *dir, | 
|  | struct dentry *dentry, umode_t mode, bool excl) | 
|  | { | 
|  | return omfs_add_node(dir, dentry, mode | S_IFREG); | 
|  | } | 
|  |  | 
|  | static struct dentry *omfs_lookup(struct inode *dir, struct dentry *dentry, | 
|  | unsigned int flags) | 
|  | { | 
|  | struct buffer_head *bh; | 
|  | struct inode *inode = NULL; | 
|  |  | 
|  | if (dentry->d_name.len > OMFS_NAMELEN) | 
|  | return ERR_PTR(-ENAMETOOLONG); | 
|  |  | 
|  | bh = omfs_find_entry(dir, dentry->d_name.name, dentry->d_name.len); | 
|  | if (!IS_ERR(bh)) { | 
|  | struct omfs_inode *oi = (struct omfs_inode *)bh->b_data; | 
|  | ino_t ino = be64_to_cpu(oi->i_head.h_self); | 
|  | brelse(bh); | 
|  | inode = omfs_iget(dir->i_sb, ino); | 
|  | } else if (bh != ERR_PTR(-ENOENT)) { | 
|  | inode = ERR_CAST(bh); | 
|  | } | 
|  | return d_splice_alias(inode, dentry); | 
|  | } | 
|  |  | 
|  | /* sanity check block's self pointer */ | 
|  | int omfs_is_bad(struct omfs_sb_info *sbi, struct omfs_header *header, | 
|  | u64 fsblock) | 
|  | { | 
|  | int is_bad; | 
|  | u64 ino = be64_to_cpu(header->h_self); | 
|  | is_bad = ((ino != fsblock) || (ino < sbi->s_root_ino) || | 
|  | (ino > sbi->s_num_blocks)); | 
|  |  | 
|  | if (is_bad) | 
|  | printk(KERN_WARNING "omfs: bad hash chain detected\n"); | 
|  |  | 
|  | return is_bad; | 
|  | } | 
|  |  | 
|  | static bool omfs_fill_chain(struct inode *dir, struct dir_context *ctx, | 
|  | u64 fsblock, int hindex) | 
|  | { | 
|  | /* follow chain in this bucket */ | 
|  | while (fsblock != ~0) { | 
|  | struct buffer_head *bh = omfs_bread(dir->i_sb, fsblock); | 
|  | struct omfs_inode *oi; | 
|  | u64 self; | 
|  | unsigned char d_type; | 
|  |  | 
|  | if (!bh) | 
|  | return true; | 
|  |  | 
|  | oi = (struct omfs_inode *) bh->b_data; | 
|  | if (omfs_is_bad(OMFS_SB(dir->i_sb), &oi->i_head, fsblock)) { | 
|  | brelse(bh); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | self = fsblock; | 
|  | fsblock = be64_to_cpu(oi->i_sibling); | 
|  |  | 
|  | /* skip visited nodes */ | 
|  | if (hindex) { | 
|  | hindex--; | 
|  | brelse(bh); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | d_type = (oi->i_type == OMFS_DIR) ? DT_DIR : DT_REG; | 
|  |  | 
|  | if (!dir_emit(ctx, oi->i_name, | 
|  | strnlen(oi->i_name, OMFS_NAMELEN), | 
|  | self, d_type)) { | 
|  | brelse(bh); | 
|  | return false; | 
|  | } | 
|  | brelse(bh); | 
|  | ctx->pos++; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | static int omfs_rename(struct mnt_idmap *idmap, struct inode *old_dir, | 
|  | struct dentry *old_dentry, struct inode *new_dir, | 
|  | struct dentry *new_dentry, unsigned int flags) | 
|  | { | 
|  | struct inode *new_inode = d_inode(new_dentry); | 
|  | struct inode *old_inode = d_inode(old_dentry); | 
|  | int err; | 
|  |  | 
|  | if (flags & ~RENAME_NOREPLACE) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (new_inode) { | 
|  | /* overwriting existing file/dir */ | 
|  | err = omfs_remove(new_dir, new_dentry); | 
|  | if (err) | 
|  | goto out; | 
|  | } | 
|  |  | 
|  | /* since omfs locates files by name, we need to unlink _before_ | 
|  | * adding the new link or we won't find the old one */ | 
|  | err = omfs_delete_entry(old_dentry); | 
|  | if (err) | 
|  | goto out; | 
|  |  | 
|  | mark_inode_dirty(old_dir); | 
|  | err = omfs_add_link(new_dentry, old_inode); | 
|  | if (err) | 
|  | goto out; | 
|  |  | 
|  | inode_set_ctime_current(old_inode); | 
|  | mark_inode_dirty(old_inode); | 
|  | out: | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static int omfs_readdir(struct file *file, struct dir_context *ctx) | 
|  | { | 
|  | struct inode *dir = file_inode(file); | 
|  | struct buffer_head *bh; | 
|  | __be64 *p; | 
|  | unsigned int hchain, hindex; | 
|  | int nbuckets; | 
|  |  | 
|  | if (ctx->pos >> 32) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (ctx->pos < 1 << 20) { | 
|  | if (!dir_emit_dots(file, ctx)) | 
|  | return 0; | 
|  | ctx->pos = 1 << 20; | 
|  | } | 
|  |  | 
|  | nbuckets = (dir->i_size - OMFS_DIR_START) / 8; | 
|  |  | 
|  | /* high 12 bits store bucket + 1 and low 20 bits store hash index */ | 
|  | hchain = (ctx->pos >> 20) - 1; | 
|  | hindex = ctx->pos & 0xfffff; | 
|  |  | 
|  | bh = omfs_bread(dir->i_sb, dir->i_ino); | 
|  | if (!bh) | 
|  | return -EINVAL; | 
|  |  | 
|  | p = (__be64 *)(bh->b_data + OMFS_DIR_START) + hchain; | 
|  |  | 
|  | for (; hchain < nbuckets; hchain++) { | 
|  | __u64 fsblock = be64_to_cpu(*p++); | 
|  | if (!omfs_fill_chain(dir, ctx, fsblock, hindex)) | 
|  | break; | 
|  | hindex = 0; | 
|  | ctx->pos = (hchain+2) << 20; | 
|  | } | 
|  | brelse(bh); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | const struct inode_operations omfs_dir_inops = { | 
|  | .lookup = omfs_lookup, | 
|  | .mkdir = omfs_mkdir, | 
|  | .rename = omfs_rename, | 
|  | .create = omfs_create, | 
|  | .unlink = omfs_remove, | 
|  | .rmdir = omfs_remove, | 
|  | }; | 
|  |  | 
|  | const struct file_operations omfs_dir_operations = { | 
|  | .read = generic_read_dir, | 
|  | .iterate_shared = omfs_readdir, | 
|  | .llseek = generic_file_llseek, | 
|  | }; |