// SPDX-License-Identifier: GPL-2.0-only
/*
 * filecheck.c
 *
 * Code which implements online file check.
 *
 * Copyright (C) 2016 SuSE.  All rights reserved.
 */

#include <linux/list.h>
#include <linux/spinlock.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kmod.h>
#include <linux/fs.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/sysctl.h>
#include <cluster/masklog.h>

#include "ocfs2.h"
#include "ocfs2_fs.h"
#include "stackglue.h"
#include "inode.h"

#include "filecheck.h"


/* File check error strings,
 * must correspond with error number in header file.
 */
static const char * const ocfs2_filecheck_errs[] = {
	"SUCCESS",
	"FAILED",
	"INPROGRESS",
	"READONLY",
	"INJBD",
	"INVALIDINO",
	"BLOCKECC",
	"BLOCKNO",
	"VALIDFLAG",
	"GENERATION",
	"UNSUPPORTED"
};

struct ocfs2_filecheck_entry {
	struct list_head fe_list;
	unsigned long fe_ino;
	unsigned int fe_type;
	unsigned int fe_done:1;
	unsigned int fe_status:31;
};

struct ocfs2_filecheck_args {
	unsigned int fa_type;
	union {
		unsigned long fa_ino;
		unsigned int fa_len;
	};
};

static const char *
ocfs2_filecheck_error(int errno)
{
	if (!errno)
		return ocfs2_filecheck_errs[errno];

	BUG_ON(errno < OCFS2_FILECHECK_ERR_START ||
	       errno > OCFS2_FILECHECK_ERR_END);
	return ocfs2_filecheck_errs[errno - OCFS2_FILECHECK_ERR_START + 1];
}

static ssize_t ocfs2_filecheck_attr_show(struct kobject *kobj,
					struct kobj_attribute *attr,
					char *buf);
static ssize_t ocfs2_filecheck_attr_store(struct kobject *kobj,
					struct kobj_attribute *attr,
					const char *buf, size_t count);
static struct kobj_attribute ocfs2_filecheck_attr_chk =
					__ATTR(check, S_IRUSR | S_IWUSR,
					ocfs2_filecheck_attr_show,
					ocfs2_filecheck_attr_store);
static struct kobj_attribute ocfs2_filecheck_attr_fix =
					__ATTR(fix, S_IRUSR | S_IWUSR,
					ocfs2_filecheck_attr_show,
					ocfs2_filecheck_attr_store);
static struct kobj_attribute ocfs2_filecheck_attr_set =
					__ATTR(set, S_IRUSR | S_IWUSR,
					ocfs2_filecheck_attr_show,
					ocfs2_filecheck_attr_store);
static struct attribute *ocfs2_filecheck_attrs[] = {
	&ocfs2_filecheck_attr_chk.attr,
	&ocfs2_filecheck_attr_fix.attr,
	&ocfs2_filecheck_attr_set.attr,
	NULL
};
ATTRIBUTE_GROUPS(ocfs2_filecheck);

static void ocfs2_filecheck_release(struct kobject *kobj)
{
	struct ocfs2_filecheck_sysfs_entry *entry = container_of(kobj,
				struct ocfs2_filecheck_sysfs_entry, fs_kobj);

	complete(&entry->fs_kobj_unregister);
}

static ssize_t
ocfs2_filecheck_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
	ssize_t ret = -EIO;
	struct kobj_attribute *kattr = container_of(attr,
					struct kobj_attribute, attr);

	kobject_get(kobj);
	if (kattr->show)
		ret = kattr->show(kobj, kattr, buf);
	kobject_put(kobj);
	return ret;
}

static ssize_t
ocfs2_filecheck_store(struct kobject *kobj, struct attribute *attr,
			const char *buf, size_t count)
{
	ssize_t ret = -EIO;
	struct kobj_attribute *kattr = container_of(attr,
					struct kobj_attribute, attr);

	kobject_get(kobj);
	if (kattr->store)
		ret = kattr->store(kobj, kattr, buf, count);
	kobject_put(kobj);
	return ret;
}

static const struct sysfs_ops ocfs2_filecheck_ops = {
	.show = ocfs2_filecheck_show,
	.store = ocfs2_filecheck_store,
};

static struct kobj_type ocfs2_ktype_filecheck = {
	.default_groups = ocfs2_filecheck_groups,
	.sysfs_ops = &ocfs2_filecheck_ops,
	.release = ocfs2_filecheck_release,
};

static void
ocfs2_filecheck_sysfs_free(struct ocfs2_filecheck_sysfs_entry *entry)
{
	struct ocfs2_filecheck_entry *p;

	spin_lock(&entry->fs_fcheck->fc_lock);
	while (!list_empty(&entry->fs_fcheck->fc_head)) {
		p = list_first_entry(&entry->fs_fcheck->fc_head,
				     struct ocfs2_filecheck_entry, fe_list);
		list_del(&p->fe_list);
		BUG_ON(!p->fe_done); /* To free a undone file check entry */
		kfree(p);
	}
	spin_unlock(&entry->fs_fcheck->fc_lock);

	kfree(entry->fs_fcheck);
	entry->fs_fcheck = NULL;
}

int ocfs2_filecheck_create_sysfs(struct ocfs2_super *osb)
{
	int ret;
	struct ocfs2_filecheck *fcheck;
	struct ocfs2_filecheck_sysfs_entry *entry = &osb->osb_fc_ent;

	fcheck = kmalloc(sizeof(struct ocfs2_filecheck), GFP_NOFS);
	if (!fcheck)
		return -ENOMEM;

	INIT_LIST_HEAD(&fcheck->fc_head);
	spin_lock_init(&fcheck->fc_lock);
	fcheck->fc_max = OCFS2_FILECHECK_MINSIZE;
	fcheck->fc_size = 0;
	fcheck->fc_done = 0;

	entry->fs_kobj.kset = osb->osb_dev_kset;
	init_completion(&entry->fs_kobj_unregister);
	ret = kobject_init_and_add(&entry->fs_kobj, &ocfs2_ktype_filecheck,
					NULL, "filecheck");
	if (ret) {
		kobject_put(&entry->fs_kobj);
		kfree(fcheck);
		return ret;
	}

	entry->fs_fcheck = fcheck;
	return 0;
}

void ocfs2_filecheck_remove_sysfs(struct ocfs2_super *osb)
{
	if (!osb->osb_fc_ent.fs_fcheck)
		return;

	kobject_del(&osb->osb_fc_ent.fs_kobj);
	kobject_put(&osb->osb_fc_ent.fs_kobj);
	wait_for_completion(&osb->osb_fc_ent.fs_kobj_unregister);
	ocfs2_filecheck_sysfs_free(&osb->osb_fc_ent);
}

static int
ocfs2_filecheck_erase_entries(struct ocfs2_filecheck_sysfs_entry *ent,
			      unsigned int count);
static int
ocfs2_filecheck_adjust_max(struct ocfs2_filecheck_sysfs_entry *ent,
			   unsigned int len)
{
	int ret;

	if ((len < OCFS2_FILECHECK_MINSIZE) || (len > OCFS2_FILECHECK_MAXSIZE))
		return -EINVAL;

	spin_lock(&ent->fs_fcheck->fc_lock);
	if (len < (ent->fs_fcheck->fc_size - ent->fs_fcheck->fc_done)) {
		mlog(ML_NOTICE,
		"Cannot set online file check maximum entry number "
		"to %u due to too many pending entries(%u)\n",
		len, ent->fs_fcheck->fc_size - ent->fs_fcheck->fc_done);
		ret = -EBUSY;
	} else {
		if (len < ent->fs_fcheck->fc_size)
			BUG_ON(!ocfs2_filecheck_erase_entries(ent,
				ent->fs_fcheck->fc_size - len));

		ent->fs_fcheck->fc_max = len;
		ret = 0;
	}
	spin_unlock(&ent->fs_fcheck->fc_lock);

	return ret;
}

#define OCFS2_FILECHECK_ARGS_LEN	24
static int
ocfs2_filecheck_args_get_long(const char *buf, size_t count,
			      unsigned long *val)
{
	char buffer[OCFS2_FILECHECK_ARGS_LEN];

	memcpy(buffer, buf, count);
	buffer[count] = '\0';

	if (kstrtoul(buffer, 0, val))
		return 1;

	return 0;
}

static int
ocfs2_filecheck_type_parse(const char *name, unsigned int *type)
{
	if (!strncmp(name, "fix", 4))
		*type = OCFS2_FILECHECK_TYPE_FIX;
	else if (!strncmp(name, "check", 6))
		*type = OCFS2_FILECHECK_TYPE_CHK;
	else if (!strncmp(name, "set", 4))
		*type = OCFS2_FILECHECK_TYPE_SET;
	else
		return 1;

	return 0;
}

static int
ocfs2_filecheck_args_parse(const char *name, const char *buf, size_t count,
			   struct ocfs2_filecheck_args *args)
{
	unsigned long val = 0;
	unsigned int type;

	/* too short/long args length */
	if ((count < 1) || (count >= OCFS2_FILECHECK_ARGS_LEN))
		return 1;

	if (ocfs2_filecheck_type_parse(name, &type))
		return 1;
	if (ocfs2_filecheck_args_get_long(buf, count, &val))
		return 1;

	if (val <= 0)
		return 1;

	args->fa_type = type;
	if (type == OCFS2_FILECHECK_TYPE_SET)
		args->fa_len = (unsigned int)val;
	else
		args->fa_ino = val;

	return 0;
}

static ssize_t ocfs2_filecheck_attr_show(struct kobject *kobj,
				    struct kobj_attribute *attr,
				    char *buf)
{

	ssize_t ret = 0, total = 0, remain = PAGE_SIZE;
	unsigned int type;
	struct ocfs2_filecheck_entry *p;
	struct ocfs2_filecheck_sysfs_entry *ent = container_of(kobj,
				struct ocfs2_filecheck_sysfs_entry, fs_kobj);

	if (ocfs2_filecheck_type_parse(attr->attr.name, &type))
		return -EINVAL;

	if (type == OCFS2_FILECHECK_TYPE_SET) {
		spin_lock(&ent->fs_fcheck->fc_lock);
		total = snprintf(buf, remain, "%u\n", ent->fs_fcheck->fc_max);
		spin_unlock(&ent->fs_fcheck->fc_lock);
		goto exit;
	}

	ret = snprintf(buf, remain, "INO\t\tDONE\tERROR\n");
	total += ret;
	remain -= ret;
	spin_lock(&ent->fs_fcheck->fc_lock);
	list_for_each_entry(p, &ent->fs_fcheck->fc_head, fe_list) {
		if (p->fe_type != type)
			continue;

		ret = snprintf(buf + total, remain, "%lu\t\t%u\t%s\n",
			       p->fe_ino, p->fe_done,
			       ocfs2_filecheck_error(p->fe_status));
		if (ret >= remain) {
			/* snprintf() didn't fit */
			total = -E2BIG;
			break;
		}
		total += ret;
		remain -= ret;
	}
	spin_unlock(&ent->fs_fcheck->fc_lock);

exit:
	return total;
}

static inline int
ocfs2_filecheck_is_dup_entry(struct ocfs2_filecheck_sysfs_entry *ent,
				unsigned long ino)
{
	struct ocfs2_filecheck_entry *p;

	list_for_each_entry(p, &ent->fs_fcheck->fc_head, fe_list) {
		if (!p->fe_done) {
			if (p->fe_ino == ino)
				return 1;
		}
	}

	return 0;
}

static inline int
ocfs2_filecheck_erase_entry(struct ocfs2_filecheck_sysfs_entry *ent)
{
	struct ocfs2_filecheck_entry *p;

	list_for_each_entry(p, &ent->fs_fcheck->fc_head, fe_list) {
		if (p->fe_done) {
			list_del(&p->fe_list);
			kfree(p);
			ent->fs_fcheck->fc_size--;
			ent->fs_fcheck->fc_done--;
			return 1;
		}
	}

	return 0;
}

static int
ocfs2_filecheck_erase_entries(struct ocfs2_filecheck_sysfs_entry *ent,
			      unsigned int count)
{
	unsigned int i = 0;
	unsigned int ret = 0;

	while (i++ < count) {
		if (ocfs2_filecheck_erase_entry(ent))
			ret++;
		else
			break;
	}

	return (ret == count ? 1 : 0);
}

static void
ocfs2_filecheck_done_entry(struct ocfs2_filecheck_sysfs_entry *ent,
			   struct ocfs2_filecheck_entry *entry)
{
	spin_lock(&ent->fs_fcheck->fc_lock);
	entry->fe_done = 1;
	ent->fs_fcheck->fc_done++;
	spin_unlock(&ent->fs_fcheck->fc_lock);
}

static unsigned int
ocfs2_filecheck_handle(struct ocfs2_super *osb,
		       unsigned long ino, unsigned int flags)
{
	unsigned int ret = OCFS2_FILECHECK_ERR_SUCCESS;
	struct inode *inode = NULL;
	int rc;

	inode = ocfs2_iget(osb, ino, flags, 0);
	if (IS_ERR(inode)) {
		rc = (int)(-(long)inode);
		if (rc >= OCFS2_FILECHECK_ERR_START &&
		    rc < OCFS2_FILECHECK_ERR_END)
			ret = rc;
		else
			ret = OCFS2_FILECHECK_ERR_FAILED;
	} else
		iput(inode);

	return ret;
}

static void
ocfs2_filecheck_handle_entry(struct ocfs2_filecheck_sysfs_entry *ent,
			     struct ocfs2_filecheck_entry *entry)
{
	struct ocfs2_super *osb = container_of(ent, struct ocfs2_super,
						osb_fc_ent);

	if (entry->fe_type == OCFS2_FILECHECK_TYPE_CHK)
		entry->fe_status = ocfs2_filecheck_handle(osb,
				entry->fe_ino, OCFS2_FI_FLAG_FILECHECK_CHK);
	else if (entry->fe_type == OCFS2_FILECHECK_TYPE_FIX)
		entry->fe_status = ocfs2_filecheck_handle(osb,
				entry->fe_ino, OCFS2_FI_FLAG_FILECHECK_FIX);
	else
		entry->fe_status = OCFS2_FILECHECK_ERR_UNSUPPORTED;

	ocfs2_filecheck_done_entry(ent, entry);
}

static ssize_t ocfs2_filecheck_attr_store(struct kobject *kobj,
				     struct kobj_attribute *attr,
				     const char *buf, size_t count)
{
	ssize_t ret = 0;
	struct ocfs2_filecheck_args args;
	struct ocfs2_filecheck_entry *entry;
	struct ocfs2_filecheck_sysfs_entry *ent = container_of(kobj,
				struct ocfs2_filecheck_sysfs_entry, fs_kobj);

	if (count == 0)
		return count;

	if (ocfs2_filecheck_args_parse(attr->attr.name, buf, count, &args))
		return -EINVAL;

	if (args.fa_type == OCFS2_FILECHECK_TYPE_SET) {
		ret = ocfs2_filecheck_adjust_max(ent, args.fa_len);
		goto exit;
	}

	entry = kmalloc(sizeof(struct ocfs2_filecheck_entry), GFP_NOFS);
	if (!entry) {
		ret = -ENOMEM;
		goto exit;
	}

	spin_lock(&ent->fs_fcheck->fc_lock);
	if (ocfs2_filecheck_is_dup_entry(ent, args.fa_ino)) {
		ret = -EEXIST;
		kfree(entry);
	} else if ((ent->fs_fcheck->fc_size >= ent->fs_fcheck->fc_max) &&
		(ent->fs_fcheck->fc_done == 0)) {
		mlog(ML_NOTICE,
		"Cannot do more file check "
		"since file check queue(%u) is full now\n",
		ent->fs_fcheck->fc_max);
		ret = -EAGAIN;
		kfree(entry);
	} else {
		if ((ent->fs_fcheck->fc_size >= ent->fs_fcheck->fc_max) &&
		    (ent->fs_fcheck->fc_done > 0)) {
			/* Delete the oldest entry which was done,
			 * make sure the entry size in list does
			 * not exceed maximum value
			 */
			BUG_ON(!ocfs2_filecheck_erase_entry(ent));
		}

		entry->fe_ino = args.fa_ino;
		entry->fe_type = args.fa_type;
		entry->fe_done = 0;
		entry->fe_status = OCFS2_FILECHECK_ERR_INPROGRESS;
		list_add_tail(&entry->fe_list, &ent->fs_fcheck->fc_head);
		ent->fs_fcheck->fc_size++;
	}
	spin_unlock(&ent->fs_fcheck->fc_lock);

	if (!ret)
		ocfs2_filecheck_handle_entry(ent, entry);

exit:
	return ret ?: count;
}
