/* SPDX-License-Identifier: (BSD-2-Clause OR GPL-2.0-or-later)
 * (C) 2019-2021 AVM GmbH <info@avm.de>
 *
 * vim:set noexpandtab shiftwidth=8 softtabstop=8 fileencoding=utf-8:
 */

/* --------------------------------------------------------------------- */

#define MODNAME "cprocfs"
#define pr_fmt(fmt) MODNAME ": " fmt

#include <linux/version.h>
#include <linux/module.h> // struct module, THIS_MODULE
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/bug.h>
#include <linux/seq_file.h>

#include <cprocfs.h>

static int cprocfs_show(struct seq_file *m, void *v)
{
	struct cprocfs_file *fp = (struct cprocfs_file *) m->private;

	(*fp->showfunc)(fp->userdata, (cprocfs_walk_cb_t) seq_printf, m);
	return 0;
}

static int cprocfs_show_open(struct inode *inode, struct file *file)
{
	/* get "data" stored in proc_dir entry, it will be assigned seq_file->private */
	return single_open(file, cprocfs_show, PDE_DATA(inode));
}

static const struct proc_ops cprocfs_show_fops = {
	.proc_open    = cprocfs_show_open,
	.proc_read    = seq_read,
	.proc_lseek   = seq_lseek,
	.proc_release = single_release,
};

/* --------------------------------------------------------------------- */

static ssize_t cprocfs_control(struct file *file,
                               const char __user *buffer,
                               size_t count,
                               loff_t *offset)
{
	struct cprocfs_file *fp;
	const char *delimitters = " \n\t";
	char control_cmd[128];
	char *argv[16];
	int argc = 0;
	char *s;
	char *next_token;

	fp = (struct cprocfs_file *) PDE_DATA(file_inode(file));

	/* Validate the length of data passed. */
	if (count >= sizeof(control_cmd))
		count = sizeof(control_cmd) - 1;

	/* Initialize the buffer before using it. */
	memset((void *) &control_cmd[0], 0, sizeof(control_cmd));
	memset((void *) &argv[0], 0, sizeof(argv));

	/* Copy from user space. */
	if (copy_from_user(&control_cmd, buffer, count))
		return -EFAULT;

	next_token = &control_cmd[0];
	s = strsep(&next_token, delimitters);
	if (s == NULL) {
		pr_err("%s:%s: no command found\n", fp->cdir->name, fp->name);
		return -EINVAL;
	}

	do {
		argv[argc++] = s;
		if (argc >= ARRAY_SIZE(argv)) {
			pr_err("%s:%s: too many parameters, dropping the command\n",
			       fp->cdir->name,
			       fp->name);
			return -EIO;
		}
		s = strsep(&next_token, delimitters);
		if (s && s[0] == 0)
			s = NULL;
	} while (s != NULL);

	(*fp->control)(fp->userdata, argc, argv);
	return count;
}

static const struct proc_ops cprocfs_control_fops = {
	.proc_open    = cprocfs_show_open,
	.proc_read    = seq_read,
	.proc_write   = cprocfs_control,
	.proc_lseek   = seq_lseek,
	.proc_release = single_release,
};

/* --------------------------------------------------------------------- */

static void _cdir_update_procfs(struct cprocfs_dir *cdir, struct proc_dir_entry *parent_proc_dir)
{
	struct cprocfs_file *fp;
	struct cprocfs_dir *cp;

	/* Beware recursion, keep stack small! */

	if (cdir->cfiles == 0 && cdir->cdirs == 0) {
		if (cdir->proc_dir) {
			remove_proc_entry(cdir->name, parent_proc_dir);
			cdir->proc_dir = 0;
		}
		return;
	}

	if (cdir->proc_dir == 0) {
		cdir->proc_dir = proc_mkdir(cdir->name, parent_proc_dir);
		if (cdir->proc_dir == 0) {
			pr_err("proc_mkdir(%s) failed\n", cdir->name);
			return;
		}
	}

	for (fp = cdir->cfiles; fp; fp = fp->next) {
		if (fp->procfs_file)
			continue;
		if (fp->control) {
			fp->procfs_file = proc_create_data(fp->name,
			                                   0644,
			                                   cdir->proc_dir,
			                                   &cprocfs_control_fops,
			                                   fp);
		} else {
			fp->procfs_file = proc_create_data(fp->name,
			                                   0444,
			                                   cdir->proc_dir,
			                                   &cprocfs_show_fops,
			                                   fp);
		}
		if (fp->procfs_file == 0)
			pr_err("%s: proc_create(%s) failed\n", cdir->name, fp->name);
	}

	for (cp = cdir->cdirs; cp; cp = cp->next)
		_cdir_update_procfs(cp, cdir->proc_dir);
}

static void cdir_update_procfs(struct cprocfs_dir *cdir)
{
	if (cdir->parent) {
		if (cdir->parent->is_root)
			_cdir_update_procfs(cdir, cdir->parent->proc_dir);
		else if (cdir->parent->proc_dir)
			_cdir_update_procfs(cdir, cdir->parent->proc_dir);
	} else {
		if (cdir->is_root)
			_cdir_update_procfs(cdir, NULL);
	}
}

static void cdir_delete_procfs(struct cprocfs_dir *cdir, struct proc_dir_entry *parent_proc_dir)
{
	struct cprocfs_file *fp;
	struct cprocfs_dir *dp;

	/* Beware recursion, keep stack small! */

	/* remove files */
	while ((fp = cdir->cfiles) != 0) {
		cdir->cfiles = fp->next;
		if (fp->procfs_file) {
			remove_proc_entry(fp->name, cdir->proc_dir);
			fp->procfs_file = 0;
		}
		kfree(fp);
	}

	/* remove subdirs */
	while ((dp = cdir->cdirs) != 0) {
		cdir->cdirs = dp->next;
		cdir_delete_procfs(dp, cdir->proc_dir);
	}

	if (cdir->proc_dir) {
		remove_proc_entry(cdir->name, parent_proc_dir);
		cdir->proc_dir = 0;
	}
	kfree(cdir);
}

static bool _cprocfs_delete_dir(struct cprocfs_dir *cdir, struct cprocfs_dir *parent)
{
	if (parent) {
		struct cprocfs_dir *dp, **pp;

		for (pp = &parent->cdirs; (dp = *pp) != 0; pp = &(*pp)->next) {
			if (dp == cdir) {
				*pp = cdir->next;
				cdir_delete_procfs(cdir, parent->proc_dir);
				return true;
			}
			if (dp->cdirs) {
				if (_cprocfs_delete_dir(cdir, dp))
					return true;
			}
		}
		return false;
	}

	cdir_delete_procfs(cdir, NULL);
	return true;
}

static void _cprocfs_attach_dir(struct cprocfs_dir *parent, struct cprocfs_dir *child)
{
	child->parent = parent;

	if (parent) {
		child->next = parent->cdirs;
		parent->cdirs = child;
		cdir_update_procfs(parent);
	} else if (child->is_root) {
		cdir_update_procfs(child);
	} else {
		pr_err("cprocfs_attach_dir(%s): no parent\n", child->name);
	}
}

static struct cprocfs_dir *_cprocfs_add_dir(struct cprocfs_dir *cdir,
                                            const char *name,
                                            bool is_root)
{
	struct cprocfs_dir *dp;

	dp = kzalloc(sizeof(struct cprocfs_dir), GFP_KERNEL);
	if (dp == 0) {
		pr_err("add dir %s failed", name);
		return NULL;
	}

	dp->name = name;
	dp->is_root = is_root;
	_cprocfs_attach_dir(cdir, dp);
	return dp;
}

/* --------------------------------------------------------------------- */

static void cprocfs_show_cdir(void *data, cprocfs_walk_cb_t cb, void *arg)
{
	struct cprocfs_dir *cdir = (struct cprocfs_dir *) data;

	(void) cprocfs_walk(cdir, cb, arg);
}

/* --------------------------------------------------------------------- */

struct cprocfs_dir *cprocfs_create_root_dir(const char *name)
{
	struct cprocfs_dir *cdir = _cprocfs_add_dir(NULL, name, true);

	if (cdir)
		(void) cprocfs_register_file(cdir, "cprocfs", cprocfs_show_cdir, cdir);

	return cdir;
}
EXPORT_SYMBOL(cprocfs_create_root_dir);

void cprocfs_destroy(struct cprocfs_dir *root)
{
	bool found = _cprocfs_delete_dir(root, root->parent);

	WARN(!found, "cprocfs: delete root dir %p: not found\n", root);
}
EXPORT_SYMBOL(cprocfs_destroy);

struct cprocfs_dir *cprocfs_add_dir(struct cprocfs_dir *cdir, const char *name)
{
	return _cprocfs_add_dir(cdir, name, false);
}
EXPORT_SYMBOL(cprocfs_add_dir);

void cprocfs_delete_dir(struct cprocfs_dir *cdir)
{
	bool found = _cprocfs_delete_dir(cdir, cdir->parent);

	WARN(!found, "cprocfs: delete dir %p: not found\n", cdir);
}
EXPORT_SYMBOL(cprocfs_delete_dir);

int cprocfs_register_control(struct cprocfs_dir *cdir,
                             const char *name,
                             void (*showfunc)(void *data, cprocfs_walk_cb_t cb, void *arg),
                             void (*control)(void *data, int argc, char *argv[]),
                             void *data)
{
	struct cprocfs_file *fp;

	fp = kzalloc(sizeof(struct cprocfs_file), GFP_KERNEL);
	if (!fp)
		return -ENOMEM;

	fp->cdir = cdir;
	fp->name = name;
	fp->userdata = data;
	fp->procfs_file = 0; /* set in _cdir_update_procfs */
	fp->showfunc = showfunc;
	fp->control = control;
	fp->next = cdir->cfiles;
	cdir->cfiles = fp;
	cdir_update_procfs(cdir);
	return 0;
}
EXPORT_SYMBOL(cprocfs_register_control);

int cprocfs_register_file(struct cprocfs_dir *cdir,
                          const char *name,
                          void (*showfunc)(void *data, cprocfs_walk_cb_t cb, void *arg),
                          void *data)
{
	return cprocfs_register_control(cdir, name, showfunc, NULL, data);
}
EXPORT_SYMBOL(cprocfs_register_file);

/* --------------------------------------------------------------------- */

struct cprocfs_dir *cprocfs_create_detached_dir(const char *name)
{
	return _cprocfs_add_dir(NULL, name, false);
}
EXPORT_SYMBOL(cprocfs_create_detached_dir);

void cprocfs_attach_dir(struct cprocfs_dir *parent, struct cprocfs_dir *child)
{
	if (child->parent == 0)
		_cprocfs_attach_dir(parent, child);
}
EXPORT_SYMBOL(cprocfs_attach_dir);

int cprocfs_rename_dir(struct cprocfs_dir *cdir, const char *name)
{
	if (cdir->proc_dir) {
		pr_err("%s: can't rename to %s: already attached\n", cdir->name, name);
		return -1;
	}
	cdir->name = name;
	return 0;
}
EXPORT_SYMBOL(cprocfs_rename_dir);

static unsigned int _cprocfs_walk(struct cprocfs_dir *cdir,
                                  const char *prefix,
                                  unsigned int level,
                                  cprocfs_walk_cb_t cb,
                                  void *arg)
{
	struct cprocfs_dir *subdir;
	struct cprocfs_file *fp;
	unsigned int count = 1;

	cb(arg,
	   "%s%*.*s%s/%s\n",
	   prefix,
	   level * 3,
	   level * 3,
	   "",
	   cdir->name,
	   (cdir->cfiles || cdir->cdirs) ? "" : " (empty)");
	level++;

	for (fp = cdir->cfiles; fp; fp = fp->next) {
		cb(arg,
		   "%s%*.*s%s%s\n",
		   prefix,
		   level * 3,
		   level * 3,
		   "",
		   fp->name,
		   fp->control ? " (control)" : "");
		count++;
	}

	for (subdir = cdir->cdirs; subdir; subdir = subdir->next)
		count += _cprocfs_walk(subdir, prefix, level, cb, arg);

	return count;
}

static int fprintk(void *arg, const char *fmt, ...)
{
	va_list args;
	int ret;

	va_start(args, fmt);
	ret = vprintk(fmt, args);
	va_end(args);
	return ret;
}

unsigned int cprocfs_walk(struct cprocfs_dir *cdir, cprocfs_walk_cb_t cb, void *arg)
{
	const char *prefix = "";

	if (cb == NULL) {
		cb = fprintk;
		prefix = MODNAME ": ";
	}
	return _cprocfs_walk(cdir, prefix, 0, cb, arg);
}
EXPORT_SYMBOL(cprocfs_walk);

/* --------------------------------------------------------------------- */

static int __init cprocfs_init_module(void)
{
	pr_info("loaded\n");
	return 0;
}

static void __exit cprocfs_exit_module(void)
{
	pr_info("unloaded\n");
}

module_init(cprocfs_init_module);
module_exit(cprocfs_exit_module);

MODULE_DESCRIPTION("AVM: easy procfs API");
MODULE_LICENSE("Dual BSD/GPL");

/* --------------------------------------------------------------------- */