| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * linux/fs/adfs/dir.c |
| * |
| * Copyright (C) 1999-2000 Russell King |
| * |
| * Common directory handling for ADFS |
| */ |
| #include <linux/slab.h> |
| #include "adfs.h" |
| |
| /* |
| * For future. This should probably be per-directory. |
| */ |
| static DECLARE_RWSEM(adfs_dir_rwsem); |
| |
| int adfs_dir_copyfrom(void *dst, struct adfs_dir *dir, unsigned int offset, |
| size_t len) |
| { |
| struct super_block *sb = dir->sb; |
| unsigned int index, remain; |
| |
| index = offset >> sb->s_blocksize_bits; |
| offset &= sb->s_blocksize - 1; |
| remain = sb->s_blocksize - offset; |
| if (index + (remain < len) >= dir->nr_buffers) |
| return -EINVAL; |
| |
| if (remain < len) { |
| memcpy(dst, dir->bhs[index]->b_data + offset, remain); |
| dst += remain; |
| len -= remain; |
| index += 1; |
| offset = 0; |
| } |
| |
| memcpy(dst, dir->bhs[index]->b_data + offset, len); |
| |
| return 0; |
| } |
| |
| int adfs_dir_copyto(struct adfs_dir *dir, unsigned int offset, const void *src, |
| size_t len) |
| { |
| struct super_block *sb = dir->sb; |
| unsigned int index, remain; |
| |
| index = offset >> sb->s_blocksize_bits; |
| offset &= sb->s_blocksize - 1; |
| remain = sb->s_blocksize - offset; |
| if (index + (remain < len) >= dir->nr_buffers) |
| return -EINVAL; |
| |
| if (remain < len) { |
| memcpy(dir->bhs[index]->b_data + offset, src, remain); |
| src += remain; |
| len -= remain; |
| index += 1; |
| offset = 0; |
| } |
| |
| memcpy(dir->bhs[index]->b_data + offset, src, len); |
| |
| return 0; |
| } |
| |
| static void __adfs_dir_cleanup(struct adfs_dir *dir) |
| { |
| dir->nr_buffers = 0; |
| |
| if (dir->bhs != dir->bh) |
| kfree(dir->bhs); |
| dir->bhs = NULL; |
| dir->sb = NULL; |
| } |
| |
| void adfs_dir_relse(struct adfs_dir *dir) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < dir->nr_buffers; i++) |
| brelse(dir->bhs[i]); |
| |
| __adfs_dir_cleanup(dir); |
| } |
| |
| static void adfs_dir_forget(struct adfs_dir *dir) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < dir->nr_buffers; i++) |
| bforget(dir->bhs[i]); |
| |
| __adfs_dir_cleanup(dir); |
| } |
| |
| int adfs_dir_read_buffers(struct super_block *sb, u32 indaddr, |
| unsigned int size, struct adfs_dir *dir) |
| { |
| struct buffer_head **bhs; |
| unsigned int i, num; |
| int block; |
| |
| num = ALIGN(size, sb->s_blocksize) >> sb->s_blocksize_bits; |
| if (num > ARRAY_SIZE(dir->bh)) { |
| /* We only allow one extension */ |
| if (dir->bhs != dir->bh) |
| return -EINVAL; |
| |
| bhs = kcalloc(num, sizeof(*bhs), GFP_KERNEL); |
| if (!bhs) |
| return -ENOMEM; |
| |
| if (dir->nr_buffers) |
| memcpy(bhs, dir->bhs, dir->nr_buffers * sizeof(*bhs)); |
| |
| dir->bhs = bhs; |
| } |
| |
| for (i = dir->nr_buffers; i < num; i++) { |
| block = __adfs_block_map(sb, indaddr, i); |
| if (!block) { |
| adfs_error(sb, "dir %06x has a hole at offset %u", |
| indaddr, i); |
| goto error; |
| } |
| |
| dir->bhs[i] = sb_bread(sb, block); |
| if (!dir->bhs[i]) { |
| adfs_error(sb, |
| "dir %06x failed read at offset %u, mapped block 0x%08x", |
| indaddr, i, block); |
| goto error; |
| } |
| |
| dir->nr_buffers++; |
| } |
| return 0; |
| |
| error: |
| adfs_dir_relse(dir); |
| |
| return -EIO; |
| } |
| |
| static int adfs_dir_read(struct super_block *sb, u32 indaddr, |
| unsigned int size, struct adfs_dir *dir) |
| { |
| dir->sb = sb; |
| dir->bhs = dir->bh; |
| dir->nr_buffers = 0; |
| |
| return ADFS_SB(sb)->s_dir->read(sb, indaddr, size, dir); |
| } |
| |
| static int adfs_dir_read_inode(struct super_block *sb, struct inode *inode, |
| struct adfs_dir *dir) |
| { |
| int ret; |
| |
| ret = adfs_dir_read(sb, ADFS_I(inode)->indaddr, inode->i_size, dir); |
| if (ret) |
| return ret; |
| |
| if (ADFS_I(inode)->parent_id != dir->parent_id) { |
| adfs_error(sb, |
| "parent directory id changed under me! (%06x but got %06x)\n", |
| ADFS_I(inode)->parent_id, dir->parent_id); |
| adfs_dir_relse(dir); |
| ret = -EIO; |
| } |
| |
| return ret; |
| } |
| |
| static void adfs_dir_mark_dirty(struct adfs_dir *dir) |
| { |
| unsigned int i; |
| |
| /* Mark the buffers dirty */ |
| for (i = 0; i < dir->nr_buffers; i++) |
| mark_buffer_dirty(dir->bhs[i]); |
| } |
| |
| static int adfs_dir_sync(struct adfs_dir *dir) |
| { |
| int err = 0; |
| int i; |
| |
| for (i = dir->nr_buffers - 1; i >= 0; i--) { |
| struct buffer_head *bh = dir->bhs[i]; |
| sync_dirty_buffer(bh); |
| if (buffer_req(bh) && !buffer_uptodate(bh)) |
| err = -EIO; |
| } |
| |
| return err; |
| } |
| |
| void adfs_object_fixup(struct adfs_dir *dir, struct object_info *obj) |
| { |
| unsigned int dots, i; |
| |
| /* |
| * RISC OS allows the use of '/' in directory entry names, so we need |
| * to fix these up. '/' is typically used for FAT compatibility to |
| * represent '.', so do the same conversion here. In any case, '.' |
| * will never be in a RISC OS name since it is used as the pathname |
| * separator. Handle the case where we may generate a '.' or '..' |
| * name, replacing the first character with '^' (the RISC OS "parent |
| * directory" character.) |
| */ |
| for (i = dots = 0; i < obj->name_len; i++) |
| if (obj->name[i] == '/') { |
| obj->name[i] = '.'; |
| dots++; |
| } |
| |
| if (obj->name_len <= 2 && dots == obj->name_len) |
| obj->name[0] = '^'; |
| |
| /* |
| * If the object is a file, and the user requested the ,xyz hex |
| * filetype suffix to the name, check the filetype and append. |
| */ |
| if (!(obj->attr & ADFS_NDA_DIRECTORY) && ADFS_SB(dir->sb)->s_ftsuffix) { |
| u16 filetype = adfs_filetype(obj->loadaddr); |
| |
| if (filetype != ADFS_FILETYPE_NONE) { |
| obj->name[obj->name_len++] = ','; |
| obj->name[obj->name_len++] = hex_asc_lo(filetype >> 8); |
| obj->name[obj->name_len++] = hex_asc_lo(filetype >> 4); |
| obj->name[obj->name_len++] = hex_asc_lo(filetype >> 0); |
| } |
| } |
| } |
| |
| static int adfs_iterate(struct file *file, struct dir_context *ctx) |
| { |
| struct inode *inode = file_inode(file); |
| struct super_block *sb = inode->i_sb; |
| const struct adfs_dir_ops *ops = ADFS_SB(sb)->s_dir; |
| struct adfs_dir dir; |
| int ret; |
| |
| down_read(&adfs_dir_rwsem); |
| ret = adfs_dir_read_inode(sb, inode, &dir); |
| if (ret) |
| goto unlock; |
| |
| if (ctx->pos == 0) { |
| if (!dir_emit_dot(file, ctx)) |
| goto unlock_relse; |
| ctx->pos = 1; |
| } |
| if (ctx->pos == 1) { |
| if (!dir_emit(ctx, "..", 2, dir.parent_id, DT_DIR)) |
| goto unlock_relse; |
| ctx->pos = 2; |
| } |
| |
| ret = ops->iterate(&dir, ctx); |
| |
| unlock_relse: |
| up_read(&adfs_dir_rwsem); |
| adfs_dir_relse(&dir); |
| return ret; |
| |
| unlock: |
| up_read(&adfs_dir_rwsem); |
| return ret; |
| } |
| |
| int |
| adfs_dir_update(struct super_block *sb, struct object_info *obj, int wait) |
| { |
| const struct adfs_dir_ops *ops = ADFS_SB(sb)->s_dir; |
| struct adfs_dir dir; |
| int ret; |
| |
| if (!IS_ENABLED(CONFIG_ADFS_FS_RW)) |
| return -EINVAL; |
| |
| if (!ops->update) |
| return -EINVAL; |
| |
| down_write(&adfs_dir_rwsem); |
| ret = adfs_dir_read(sb, obj->parent_id, 0, &dir); |
| if (ret) |
| goto unlock; |
| |
| ret = ops->update(&dir, obj); |
| if (ret) |
| goto forget; |
| |
| ret = ops->commit(&dir); |
| if (ret) |
| goto forget; |
| up_write(&adfs_dir_rwsem); |
| |
| adfs_dir_mark_dirty(&dir); |
| |
| if (wait) |
| ret = adfs_dir_sync(&dir); |
| |
| adfs_dir_relse(&dir); |
| return ret; |
| |
| /* |
| * If the updated failed because the entry wasn't found, we can |
| * just release the buffers. If it was any other error, forget |
| * the dirtied buffers so they aren't written back to the media. |
| */ |
| forget: |
| if (ret == -ENOENT) |
| adfs_dir_relse(&dir); |
| else |
| adfs_dir_forget(&dir); |
| unlock: |
| up_write(&adfs_dir_rwsem); |
| |
| return ret; |
| } |
| |
| static unsigned char adfs_tolower(unsigned char c) |
| { |
| if (c >= 'A' && c <= 'Z') |
| c += 'a' - 'A'; |
| return c; |
| } |
| |
| static int __adfs_compare(const unsigned char *qstr, u32 qlen, |
| const char *str, u32 len) |
| { |
| u32 i; |
| |
| if (qlen != len) |
| return 1; |
| |
| for (i = 0; i < qlen; i++) |
| if (adfs_tolower(qstr[i]) != adfs_tolower(str[i])) |
| return 1; |
| |
| return 0; |
| } |
| |
| static int adfs_dir_lookup_byname(struct inode *inode, const struct qstr *qstr, |
| struct object_info *obj) |
| { |
| struct super_block *sb = inode->i_sb; |
| const struct adfs_dir_ops *ops = ADFS_SB(sb)->s_dir; |
| const unsigned char *name; |
| struct adfs_dir dir; |
| u32 name_len; |
| int ret; |
| |
| down_read(&adfs_dir_rwsem); |
| ret = adfs_dir_read_inode(sb, inode, &dir); |
| if (ret) |
| goto unlock; |
| |
| ret = ops->setpos(&dir, 0); |
| if (ret) |
| goto unlock_relse; |
| |
| ret = -ENOENT; |
| name = qstr->name; |
| name_len = qstr->len; |
| while (ops->getnext(&dir, obj) == 0) { |
| if (!__adfs_compare(name, name_len, obj->name, obj->name_len)) { |
| ret = 0; |
| break; |
| } |
| } |
| obj->parent_id = ADFS_I(inode)->indaddr; |
| |
| unlock_relse: |
| up_read(&adfs_dir_rwsem); |
| adfs_dir_relse(&dir); |
| return ret; |
| |
| unlock: |
| up_read(&adfs_dir_rwsem); |
| return ret; |
| } |
| |
| const struct file_operations adfs_dir_operations = { |
| .read = generic_read_dir, |
| .llseek = generic_file_llseek, |
| .iterate_shared = adfs_iterate, |
| .fsync = generic_file_fsync, |
| }; |
| |
| static int |
| adfs_hash(const struct dentry *parent, struct qstr *qstr) |
| { |
| const unsigned char *name; |
| unsigned long hash; |
| u32 len; |
| |
| if (qstr->len > ADFS_SB(parent->d_sb)->s_namelen) |
| return -ENAMETOOLONG; |
| |
| len = qstr->len; |
| name = qstr->name; |
| hash = init_name_hash(parent); |
| while (len--) |
| hash = partial_name_hash(adfs_tolower(*name++), hash); |
| qstr->hash = end_name_hash(hash); |
| |
| return 0; |
| } |
| |
| /* |
| * Compare two names, taking note of the name length |
| * requirements of the underlying filesystem. |
| */ |
| static int adfs_compare(const struct dentry *dentry, unsigned int len, |
| const char *str, const struct qstr *qstr) |
| { |
| return __adfs_compare(qstr->name, qstr->len, str, len); |
| } |
| |
| const struct dentry_operations adfs_dentry_operations = { |
| .d_hash = adfs_hash, |
| .d_compare = adfs_compare, |
| }; |
| |
| static struct dentry * |
| adfs_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags) |
| { |
| struct inode *inode = NULL; |
| struct object_info obj; |
| int error; |
| |
| error = adfs_dir_lookup_byname(dir, &dentry->d_name, &obj); |
| if (error == 0) { |
| /* |
| * This only returns NULL if get_empty_inode |
| * fails. |
| */ |
| inode = adfs_iget(dir->i_sb, &obj); |
| if (!inode) |
| inode = ERR_PTR(-EACCES); |
| } else if (error != -ENOENT) { |
| inode = ERR_PTR(error); |
| } |
| return d_splice_alias(inode, dentry); |
| } |
| |
| /* |
| * directories can handle most operations... |
| */ |
| const struct inode_operations adfs_dir_inode_operations = { |
| .lookup = adfs_lookup, |
| .setattr = adfs_notify_change, |
| }; |