blob: 77fbd196008f6a16cd1a7b9c29a23ce49bec215a [file] [log] [blame]
// 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,
};