/* SPDX-License-Identifier: (BSD-2-Clause OR GPL-2.0-or-later) * (C) 2019-2021 AVM GmbH * * vim:set noexpandtab shiftwidth=8 softtabstop=8 fileencoding=utf-8: */ /* --------------------------------------------------------------------- */ #define MODNAME "cprocfs" #define pr_fmt(fmt) MODNAME ": " fmt #include #include // struct module, THIS_MODULE #include #include #include #include #include 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"); /* --------------------------------------------------------------------- */