root/fs/adfs/dir.c
// 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/hex.h>
#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 = kzalloc_objs(*bhs, num);
                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,
};