| // 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); |
| |
| dir->i_ctime = current_time(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; |
| |
| old_inode->i_ctime = current_time(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, |
| }; |