/*
 *  Information interface for ALSA driver
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include "driver.h"
#include "minors.h"
#include "info.h"
#include "sndpersist.h"
#include <stdarg.h>

/*

 */

int snd_info_check_reserved_words(const char *str)
{
	static char *reserved[] =
	{
		"dev",
		"version",
		"meminfo",
		"memdebug",
		"detect",
		"devices",
		"oss-devices",
		"cards",
		"timers",
		"synth",
		"pcm",
		"seq",
		NULL
	};
	char **xstr = reserved;

	while (*xstr) {
		if (!strcmp(*xstr, str))
			return 0;
		xstr++;
	}
	return 1;
}

#ifdef CONFIG_PROC_FS

extern int snd_major;
extern struct file_operations snd_fops;

snd_mutex_define_static(info);

struct snd_info_private_data {
	snd_info_buffer_t *rbuffer;
	snd_info_buffer_t *wbuffer;
	snd_info_entry_t *entry;
	void *file_private_data;
};

static snd_info_entry_t *snd_info_entries;
static snd_info_entry_t *snd_info_devices;

static int snd_info_version_init(void);
static int snd_info_version_done(void);

/*

 */

int snd_iprintf(snd_info_buffer_t * buffer, char *fmt,...)
{
	va_list args;
	int res;
	char sbuffer[512];

	if (buffer->stop || buffer->error)
		return 0;
	va_start(args, fmt);
	res = vsprintf(sbuffer, fmt, args);
	va_end(args);
	if (buffer->size + res >= buffer->len) {
		buffer->stop = 1;
		return 0;
	}
	strcpy(buffer->curr, sbuffer);
	buffer->curr += res;
	buffer->size += res;
	return res;
}

/*

 */

struct proc_dir_entry *snd_proc_root = NULL;
struct proc_dir_entry *snd_proc_dev = NULL;
struct proc_dir_entry *snd_proc_seq = NULL;

#ifndef LINUX_2_1

static struct inode *snd_info_get_inode(struct super_block *s, int ino, struct proc_dir_entry *de)
{
	struct inode *inode = iget(s, ino);
	if (inode && inode->i_sb == s) {
		inode->u.generic_ip = (void *) de;
		if (de) {
			if (de->mode) {
				inode->i_mode = de->mode;
				inode->i_uid = de->uid;
				inode->i_gid = de->gid;
			}
			if (de->size)
				inode->i_size = de->size;
			if (de->ops)
				inode->i_op = de->ops;
			if (de->nlink)
				inode->i_nlink = de->nlink;
			if (de->fill_inode)
				de->fill_inode(inode);
		}
	}
	return inode;
}

static int snd_info_match(int len, const char *name, struct proc_dir_entry *de)
{
	if (!de || !de->low_ino)
		return 0;
	/* "" means "." ---> so paths like "/usr/lib//libc.a" work */
	if (!len && (de->name[0] == '.') && (de->name[1] == '\0'))
		return 1;
	if (de->namelen != len)
		return 0;
	return !memcmp(name, de->name, len);
}

static int snd_info_lookup(struct inode *dir, const char *name, int len,
			   struct inode **result)
{
	struct proc_dir_entry *de;
	int ino;

	*result = NULL;
	if (!dir || !S_ISDIR(dir->i_mode)) {
		iput(dir);
		return -ENOTDIR;
	}
	de = (struct proc_dir_entry *) dir->u.generic_ip;
	if (!de) {
		iput(dir);
		return -EINVAL;
	}
	/* Special case "." and "..": they aren't on the directory list */
	*result = dir;
	if (!len)
		return 0;
	if (name[0] == '.') {
		if (len == 1)
			return 0;
		if (name[1] == '.' && len == 2) {
			struct inode *inode;
			inode = snd_info_get_inode(dir->i_sb,
						   de->parent->low_ino,
						   de->parent);
			iput(dir);
			if (!inode)
				return -EINVAL;
			*result = inode;
			return 0;
		}
	}
	*result = NULL;
	for (de = de->subdir; de; de = de->next) {
		if (snd_info_match(len, name, de))
			break;
	}
	if (!de) {
		iput(dir);
		return -ENOENT;
	}
	ino = de->low_ino | (dir->i_ino & ~(0xffff));

	if (!(*result = snd_info_get_inode(dir->i_sb, ino, de))) {
		iput(dir);
		return -EINVAL;
	}
	iput(dir);
	return 0;
}

/*
 * This returns non-zero if at EOF, so that the /proc
 * root directory can use this and check if it should
 * continue with the <pid> entries..
 *
 * Note that the VFS-layer doesn't care about the return
 * value of the readdir() call, as long as it's non-negative
 * for success..
 */
static int snd_info_readdir(struct inode *inode, struct file *filp,
			    void *dirent, filldir_t filldir)
{
	struct proc_dir_entry *de;
	unsigned int ino;
	int i;

	if (!inode || !S_ISDIR(inode->i_mode))
		return -ENOTDIR;
	ino = inode->i_ino;
	de = (struct proc_dir_entry *) inode->u.generic_ip;
	if (!de)
		return -EINVAL;
	i = filp->f_pos;
	switch (i) {
	case 0:
		if (filldir(dirent, ".", 1, i, ino) < 0)
			return 0;
		i++;
		filp->f_pos++;
		/* fall through */
	case 1:
		if (filldir(dirent, "..", 2, i, de->parent->low_ino) < 0)
			return 0;
		i++;
		filp->f_pos++;
		/* fall through */
	default:
		ino &= ~0xffff;
		de = de->subdir;
		i -= 2;
		for (;;) {
			if (!de)
				return 1;
			if (!i)
				break;
			de = de->next;
			i--;
		}

		do {
			if (filldir(dirent, de->name, de->namelen,
				    filp->f_pos, ino | de->low_ino) < 0)
				return 0;
			filp->f_pos++;
			de = de->next;
		} while (de);
	}
	return 1;
}

static struct file_operations snd_info_dir_operations =
{
	NULL,			/* lseek - default */
	NULL,			/* read - bad */
	NULL,			/* write - bad */
	snd_info_readdir,	/* readdir */
	NULL,			/* select - default */
	NULL,			/* ioctl - default */
	NULL,			/* mmap */
	NULL,			/* no special open code */
	NULL,			/* no special release code */
	NULL			/* can't fsync */
};

static struct inode_operations snd_info_dir_inode_operations =
{
	&snd_info_dir_operations,	/* default net directory file-ops */
	NULL,				/* create */
	snd_info_lookup,		/* lookup */
	NULL,				/* link */
	NULL,				/* unlink */
	NULL,				/* symlink */
	NULL,				/* mkdir */
	NULL,				/* rmdir */
	NULL,				/* mknod */
	NULL,				/* rename */
	NULL,				/* readlink */
	NULL,				/* follow_link */
	NULL,				/* readpage */
	NULL,				/* writepage */
	NULL,				/* bmap */
	NULL,				/* truncate */
	NULL				/* permission */
};

#else /* LINUX_2_1 */

static void snd_info_fill_inode(struct inode *inode, int fill)
{
	if (fill)
		MOD_INC_USE_COUNT;
	else
		MOD_DEC_USE_COUNT;
}

#endif

static snd_info_entry_t *snd_info_find_entry(unsigned short ino)
{
	int tmp;
	snd_card_t *card;
	snd_info_entry_t *entry;

	for (entry = snd_info_entries; entry; entry = entry->next) {
		if (!entry->p)
			continue;
		if (entry->p->low_ino == ino)
			break;
	}
	if (!entry) {
		for (tmp = 0; tmp < SND_CARDS && !entry; tmp++) {
			card = snd_cards[tmp];
			if (!card)
				continue;
			for (entry = card->info_entries; entry;
							entry = entry->next) {
				if (!entry->p)
					continue;
				if (entry->p->low_ino == ino)
					break;
			}
		}
	}
	return entry;
}

static snd_card_t *snd_info_find_card(unsigned short ino)
{
	int tmp;
	snd_card_t *card;

	for (tmp = 0; tmp < SND_CARDS; tmp++) {
		card = snd_cards[tmp];
		if (!card || !card->proc_dir_link)
			continue;
		if (card->proc_dir_link->low_ino == ino)
			return card;
	}
	return NULL;
}

#ifdef LINUX_2_1
static loff_t snd_info_entry_lseek(struct file *file, loff_t offset, int orig)
#else
static int snd_info_entry_lseek(struct inode *inode, struct file *file,
				off_t offset, int orig)
#endif
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return -EIO;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		switch (orig) {
		case 0:	/* SEEK_SET */
			file->f_pos = offset;
			return file->f_pos;
		case 1:	/* SEEK_CUR */
			file->f_pos += offset;
			return file->f_pos;
		case 2:	/* SEEK_END */
		default:
			return -EINVAL;
		}
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.lseek)
			return entry->t.data.lseek(entry->private_data,
						   data->file_private_data,
						   file, offset, orig);
		break;
	}
	return -ENXIO;
}

#ifdef LINUX_2_1
static ssize_t snd_info_entry_read(struct file *file, char *buffer,
				   size_t count, loff_t * offset)
#else
static int snd_info_entry_read(struct inode *inode, struct file *file,
			       char *buffer, int count)
#endif
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;
	snd_info_buffer_t *buf;
	long size = 0, size1;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return -EIO;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		buf = data->rbuffer;
		if (!buf)
			return -EIO;
		if (file->f_pos >= buf->size)
			return 0;
		size = buf->size < count ? buf->size : count;
		size1 = buf->size - file->f_pos;
		if (size1 < size)
			size = size1;
		if (verify_area(VERIFY_WRITE, buffer, size))
			return -EFAULT;
		copy_to_user(buffer, buf->buffer + file->f_pos, size);
		file->f_pos += size;
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.read)
			return entry->t.data.read(entry->private_data,
						  data->file_private_data,
						  file, buffer, count);
		if (size > 0)
			file->f_pos += size;
		break;
	}
	return size;
}

#ifdef LINUX_2_1
static ssize_t snd_info_entry_write(struct file *file, const char *buffer,
				    size_t count, loff_t * offset)
#else
static int snd_info_entry_write(struct inode *inode, struct file *file,
				const char *buffer, int count)
#endif
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;
	snd_info_buffer_t *buf;
	long size = 0, size1;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return -EIO;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		buf = data->wbuffer;
		if (!buf)
			return -EIO;
		if (file->f_pos < 0)
			return -EINVAL;
		if (file->f_pos >= buf->len)
			return -ENOMEM;
		size = buf->len < count ? buf->len : count;
		size1 = buf->len - file->f_pos;
		if (size1 < size)
			size = size1;
		if (verify_area(VERIFY_READ, buffer, size))
			return -EFAULT;
		copy_from_user(buf->buffer + file->f_pos, buffer, size);
		if (buf->size < file->f_pos + size)
			buf->size = file->f_pos + size;
		file->f_pos += size;
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.write)
			return entry->t.data.write(entry->private_data,
						   data->file_private_data,
						   file, buffer, count);
		if (size > 0)
			file->f_pos += size;
		break;
	}
	return size;
}

static int snd_info_entry_open(struct inode *inode, struct file *file)
{
	snd_info_entry_t *entry;
	struct snd_info_private_data *data;
	snd_info_buffer_t *buffer;
	int mode, err;

	snd_mutex_down_static(info);
	entry = snd_info_find_entry(inode->i_ino);
	if (!entry) {
		snd_mutex_up_static(info);
		return -ENODEV;
	}
	mode = file->f_flags & O_ACCMODE;
	if (mode == O_RDONLY || mode == O_RDWR) {
		if ((entry->type == SND_INFO_ENTRY_TEXT &&
					!entry->t.text.read_size) ||
		    (entry->type == SND_INFO_ENTRY_DATA &&
		    			!entry->t.data.read) ||
		    entry->type == SND_INFO_ENTRY_DEVICE) {
			snd_mutex_up_static(info);
			return -ENODEV;
		}
	}
	if (mode == O_WRONLY || mode == O_RDWR) {
		if ((entry->type == SND_INFO_ENTRY_TEXT &&
					!entry->t.text.write_size) ||
		    (entry->type == SND_INFO_ENTRY_DATA &&
		    			!entry->t.data.write) ||
		    entry->type == SND_INFO_ENTRY_DEVICE) {
			snd_mutex_up_static(info);
			return -ENODEV;
		}
	}
	data = (struct snd_info_private_data *)
			snd_calloc(sizeof(struct snd_info_private_data));
	if (!data) {
		snd_mutex_up_static(info);
		return -ENOMEM;
	}
	data->entry = entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		if (mode == O_RDONLY || mode == O_RDWR) {
			buffer = (snd_info_buffer_t *)
				 	snd_calloc(sizeof(snd_info_buffer_t));
			if (!buffer) {
				snd_free(data, sizeof(struct snd_info_private_data));
				snd_mutex_up_static(info);
				return -ENOMEM;
			}
			buffer->len = (entry->t.text.read_size +
				      (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
			buffer->buffer = vmalloc(buffer->len);
			if (!buffer->buffer) {
				snd_free(buffer, sizeof(snd_info_buffer_t));
				snd_free(data, sizeof(struct snd_info_private_data));
				snd_mutex_up_static(info);
				return -ENOMEM;
			}
			buffer->curr = buffer->buffer;
			data->rbuffer = buffer;
		}
		if (mode == O_WRONLY || mode == O_RDWR) {
			buffer = (snd_info_buffer_t *)
					snd_calloc(sizeof(snd_info_buffer_t));
			if (!buffer) {
				if (mode == O_RDWR) {
					vfree(data->rbuffer->buffer);
					snd_free(data->rbuffer, sizeof(snd_info_buffer_t));
				}
				snd_free(data, sizeof(struct snd_info_private_data));
				snd_mutex_up_static(info);
				return -ENOMEM;
			}
			buffer->len = (entry->t.text.write_size +
				      (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
			buffer->buffer = vmalloc(buffer->len);
			if (!buffer->buffer) {
				if (mode == O_RDWR) {
					vfree(data->rbuffer->buffer);
					snd_free(data->rbuffer, sizeof(snd_info_buffer_t));
				}
				snd_free(buffer, sizeof(snd_info_buffer_t));
				snd_free(data, sizeof(struct snd_info_private_data));
				snd_mutex_up_static(info);
				return -ENOMEM;
			}
			buffer->curr = buffer->buffer;
			data->wbuffer = buffer;
		}
		break;
	case SND_INFO_ENTRY_DATA:	/* data */
		if (entry->t.data.open) {
			if ((err = entry->t.data.open(entry->private_data,
						      entry, mode,
						      &data->file_private_data)) < 0) {
				snd_free(data, sizeof(struct snd_info_private_data));
				snd_mutex_up_static(info);
				return err;
			}
		}
		break;
	}
	file->private_data = data;
	MOD_INC_USE_COUNT;
	if (entry->use_inc)
		entry->use_inc(entry->card);
	snd_mutex_up_static(info);
	if (entry->type == SND_INFO_ENTRY_TEXT &&
	    (mode == O_RDONLY || mode == O_RDWR)) {
		if (entry->t.text.read) {
			snd_mutex_down(entry, access);
			entry->t.text.read(data->rbuffer, entry->private_data);
			snd_mutex_up(entry, access);
		}
	}
	return 0;
}

static int snd_info_entry_release(struct inode *inode, struct file *file)
{
	snd_info_entry_t *entry;
	struct snd_info_private_data *data;
	int mode;

	mode = file->f_flags & O_ACCMODE;
	if ((data = (struct snd_info_private_data *) file->private_data) == NULL) {
		MOD_DEC_USE_COUNT;
		return -EINVAL;
	}
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		if (mode == O_RDONLY || mode == O_RDWR) {
			vfree(data->rbuffer->buffer);
			snd_free(data->rbuffer, sizeof(snd_info_buffer_t));
		}
		if (mode == O_WRONLY || mode == O_RDWR) {
			if (entry->t.text.write) {
				entry->t.text.write(data->wbuffer,
				                    entry->private_data);
				if (data->wbuffer->error) {
					snd_printk("data write error to /proc/asound/%s%s%s (%i)\n",
						entry->card ? entry->card->id : "",
						entry->card ? "/" : "", entry->name,
						data->wbuffer->error);
				}
			}
			vfree(data->wbuffer->buffer);
			snd_free(data->wbuffer, sizeof(snd_info_buffer_t));
		}
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.release)
			entry->t.data.release(entry->private_data,
					      entry, mode,
					      data->file_private_data);
		break;
	}
	if (entry->use_dec)
		entry->use_dec(entry->card);
	MOD_DEC_USE_COUNT;
	snd_free(data, sizeof(struct snd_info_private_data));
	return 0;
}

#ifdef SND_POLL
static unsigned int snd_info_entry_poll(struct file *file, poll_table * wait)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;
	unsigned int mask;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	mask = 0;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.poll)
			return entry->t.data.poll(entry->private_data,
						  data->file_private_data,
						  file, wait);
		if (entry->t.data.read)
			mask |= POLLIN | POLLRDNORM;
		if (entry->t.data.write)
			mask |= POLLOUT | POLLWRNORM;
		break;
	}
	return mask;
}
#else
static int snd_info_entry_select(struct inode *inode, struct file *file,
			         int sel_type, select_table * wait)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.select)
			return entry->t.data.select(entry->private_data,
						    data->file_private_data,
						    file, sel_type, wait);
		if (sel_type == SEL_IN && entry->t.data.read)
			return 1;
		if (sel_type == SEL_OUT && entry->t.data.write)
			return 1;
		break;
	}
	return 0;
}
#endif

static int snd_info_entry_ioctl(struct inode *inode, struct file *file,
				unsigned int cmd, unsigned long arg)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.ioctl)
			return entry->t.data.ioctl(entry->private_data,
						   data->file_private_data,
						   file, cmd, arg);
		break;
	}
	return -ENOTTY;
}

#ifdef LINUX_2_1
static int snd_info_entry_mmap(struct file *file, struct vm_area_struct *vma)
#else
static int snd_info_entry_mmap(struct inode *inode, struct file *file,
			       struct vm_area_struct *vma)
#endif
{
#ifdef LINUX_2_1
	struct inode *inode = file->f_dentry->d_inode;
#endif
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.mmap)
			return entry->t.data.mmap(entry->private_data,
						  data->file_private_data,
						  inode, file, vma);
		break;
	}
	return -ENXIO;
}

static struct file_operations snd_info_entry_operations =
{
	snd_info_entry_lseek,	/* lseek */
	snd_info_entry_read,	/* read */
	snd_info_entry_write,	/* write */
	NULL,			/* readdir */
#ifdef SND_POLL
	snd_info_entry_poll,	/* poll */
#else
	snd_info_entry_select,	/* select */
#endif
	snd_info_entry_ioctl,	/* ioctl - default */
	snd_info_entry_mmap,	/* mmap */
	snd_info_entry_open,	/* open */
#ifdef SND_FOPS_FLUSH
	NULL,			/* flush */
#endif
#ifdef LINUX_2_1
	snd_info_entry_release,	/* release */
#else
	(void (*)(struct inode * inode, struct file * file))
			snd_info_entry_release,	/* release */
#endif
	NULL,			/* can't fsync */
#ifdef LINUX_2_1
	NULL,			/* fasync */
	NULL,			/* check_media_change */
	NULL,			/* revalidate */
	NULL,			/* lock */
#endif
};

static struct inode_operations snd_info_entry_inode_operations =
{
	&snd_info_entry_operations,	/* default sound info directory file-ops */
	NULL,			/* create */
	NULL,			/* lookup */
	NULL,			/* link */
	NULL,			/* unlink */
	NULL,			/* symlink */
	NULL,			/* mkdir */
	NULL,			/* rmdir */
	NULL,			/* mknod */
	NULL,			/* rename */
	NULL,			/* readlink */
	NULL,			/* follow_link */
	NULL,			/* readpage */
	NULL,			/* writepage */
	NULL,			/* bmap */
	NULL,			/* truncate */
	NULL			/* permission */
};

static struct inode_operations snd_info_device_inode_operations =
{
	&snd_fops,		/* default sound info directory file-ops */
	NULL,			/* create */
	NULL,			/* lookup */
	NULL,			/* link */
	NULL,			/* unlink */
	NULL,			/* symlink */
	NULL,			/* mkdir */
	NULL,			/* rmdir */
	NULL,			/* mknod */
	NULL,			/* rename */
	NULL,			/* readlink */
	NULL,			/* follow_link */
	NULL,			/* readpage */
	NULL,			/* writepage */
	NULL,			/* bmap */
	NULL,			/* truncate */
	NULL			/* permission */
};

#ifdef LINUX_2_1
static int snd_info_card_readlink(struct dentry *dentry,
				  char *buffer, int buflen)
{
	int len;
	snd_card_t *card;

	snd_mutex_down_static(info);
	card = snd_info_find_card(dentry->d_inode->i_ino);
	if (!card) {
		snd_mutex_up_static(info);
		return -ENOENT;
	}
	len = strlen(card->id) + 1;
	if (buflen < len)
		len = buflen;
	copy_to_user(buffer, card->id, len);
	snd_mutex_up_static(info);
	return len;
}

static struct dentry *snd_info_card_followlink(struct dentry *dentry,
					       struct dentry *base,
					       unsigned int follow)
{
	snd_card_t *card;
	char tmp[16];

	snd_mutex_down_static(info);
	card = snd_info_find_card(dentry->d_inode->i_ino);
	if (!card || !card->proc_dir) {
		snd_mutex_up_static(info);
		return NULL;
	}
	strcpy(tmp, card->id);
	snd_mutex_up_static(info);
	return lookup_dentry(tmp, base, follow);
}
#else
static int snd_info_card_readlink(struct inode *inode,
				  char *buffer, int buflen)
{
	int len;
	snd_card_t *card;

	iput(inode);
	snd_mutex_down_static(info);
	card = snd_info_find_card(inode->i_ino);
	if (!card) {
		snd_mutex_up_static(info);
		return -ENOENT;
	}
	len = strlen(card->id) + 1;
	if (buflen < len)
		len = buflen;
	copy_to_user(buffer, card->id, len);
	snd_mutex_up_static(info);
	return len;
}

static int snd_info_card_followlink(struct inode *dir, struct inode *inode,
				    int flag, int mode,
				    struct inode **res_inode)
{
	struct inode *in;
	snd_card_t *card;
	struct proc_dir_entry *entry;

	*res_inode = NULL;
	iput(dir);
	snd_mutex_down_static(info);
	card = snd_info_find_card(inode->i_ino);
	if (!card || !(entry = card->proc_dir)) {
		snd_mutex_up_static(info);
		return -ENOENT;
	}
	in = snd_info_get_inode(inode->i_sb, entry->low_ino, entry);
	iput(inode);
	snd_mutex_up_static(info);
	if (!in)
		return -ENOENT;
	*res_inode = in;
	return 0;
}
#endif

static struct file_operations snd_info_card_link_operations =
{
	NULL,			/* lseek - default */
	NULL,			/* read - bad */
	NULL,			/* write - bad */
	NULL,			/* readdir - bad */
	NULL,			/* select - default */
	NULL,			/* ioctl - default */
	NULL,			/* mmap */
	NULL,			/* very special open code */
	NULL,			/* no special release code */
	NULL			/* can't fsync */
};

struct inode_operations snd_info_card_link_inode_operations =
{
	&snd_info_card_link_operations,		/* file-operations */
	NULL,			/* create */
	NULL,			/* lookup */
	NULL,			/* link */
	NULL,			/* unlink */
	NULL,			/* symlink */
	NULL,			/* mkdir */
	NULL,			/* rmdir */
	NULL,			/* mknod */
	NULL,			/* rename */
	snd_info_card_readlink,	/* readlink */
	snd_info_card_followlink, /* follow_link */
	NULL,			/* readpage */
	NULL,			/* writepage */
	NULL,			/* bmap */
	NULL,			/* truncate */
	NULL			/* permission */
};


#ifndef LINUX_2_1
static struct proc_dir_entry snd_info_proc_root =
{
	0, 6, "asound",		/* inode, namelen, name */
	S_IFDIR | S_IRUGO | S_IXUGO, 2, 0, 0,	/* mode, nlink, uid, gid */
	0, &snd_info_dir_inode_operations,	/* size, ops */
	NULL, NULL,		/* get_info, fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
static struct proc_dir_entry snd_info_proc_dev =
{
	0, 3, "dev",		/* inode, namelen, name */
	S_IFDIR | S_IRUGO | S_IXUGO, 2, 0, 0,	/* mode, nlink, uid, gid */
	0, &snd_info_dir_inode_operations,	/* size, ops */
	NULL, NULL,		/* get_info, fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
static struct proc_dir_entry snd_info_proc_seq =
{
	0, 3, "seq",		/* inode, namelen, name */
	S_IFDIR | S_IRUGO | S_IXUGO, 2, 0, 0,	/* mode, nlink, uid, gid */
	0, &snd_info_dir_inode_operations,	/* size, ops */
	NULL, NULL,		/* get_info, fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
#endif

int snd_info_init(void)
{
#ifdef LINUX_2_1
	struct proc_dir_entry *p;
#endif

	snd_info_entries = NULL;
	snd_info_devices = NULL;
#ifdef LINUX_2_1
	p = create_proc_entry("asound", S_IFDIR | S_IRUGO | S_IXUGO, &proc_root);
	if (!p)
		return -ENOMEM;
	p->fill_inode = snd_info_fill_inode;
	snd_proc_root = p;
	p = create_proc_entry("dev", S_IFDIR | S_IRUGO | S_IXUGO, snd_proc_root);
	if (!p)
		return -ENOMEM;
	p->fill_inode = snd_info_fill_inode;
	snd_proc_dev = p;
	p = create_proc_entry("seq", S_IFDIR | S_IRUGO | S_IXUGO, snd_proc_root);
	if (!p)
		return -ENOMEM;
	p->fill_inode = snd_info_fill_inode;
	snd_proc_seq = p;
#else
	if (proc_register_dynamic(&proc_root, &snd_info_proc_root) < 0) {
		return -EAGAIN;
	}
	snd_proc_root = &snd_info_proc_root;
	if (proc_register_dynamic(snd_proc_root, &snd_info_proc_dev) < 0) {
		return -EAGAIN;
	}
	snd_proc_dev = &snd_info_proc_dev;
	if (proc_register_dynamic(snd_proc_root, &snd_info_proc_seq) < 0) {
		return -EAGAIN;
	}
	snd_proc_seq = &snd_info_proc_seq;
#endif
	snd_info_version_init();
	snd_memory_info_init();
	snd_minor_info_init();
#ifdef CONFIG_SND_OSSEMUL
	snd_minor_info_oss_init();
#endif
	snd_card_info_init();
	return 0;
}

int snd_info_done(void)
{
	snd_card_info_done();
#ifdef CONFIG_SND_OSSEMUL
	snd_minor_info_oss_done();
#endif
	snd_minor_info_done();
	snd_memory_info_done();
	snd_info_version_done();
#ifdef CONFIG_SND_DEBUG
	if (snd_info_devices) {
		snd_printd("Oops.. Info device entries still allocated!!!\n");
	}
	if (snd_info_entries) {
		snd_printd("Oops.. Info entries still allocated!!!\n");
	}
#endif
	if (snd_proc_root) {
		if (snd_proc_seq)
			proc_unregister(snd_proc_seq, snd_proc_seq->low_ino);
		if (snd_proc_dev)
			proc_unregister(snd_proc_dev, snd_proc_dev->low_ino);
		proc_unregister(&proc_root, snd_proc_root->low_ino);
	}
	return 0;
}

/*

 */

#ifndef LINUX_2_1
static struct proc_dir_entry snd_info_proc_card_template =
{
	0, 0, 0,		/* inode, namelen, name */
	S_IFDIR | S_IRUGO | S_IXUGO, 2, 0, 0,	/* mode, nlink, uid, gid */
	0, &snd_info_dir_inode_operations,	/* size, ops */
	NULL, NULL,		/* get_info, fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
static struct proc_dir_entry snd_info_proc_card_link_template =
{
	0, 0, 0,		/* inode, namelen, name */
	S_IFLNK | S_IRUGO | S_IWUGO | S_IXUGO, 1, 0, 0,	/* mode, nlink, uid, gid */
	0, &snd_info_card_link_inode_operations,	/* size, ops */
	NULL, NULL,		/* get_info, fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
#endif

int snd_info_card_register(snd_card_t * card)
{
	char str[4];
#ifdef LINUX_2_1
	struct proc_dir_entry *p;
#endif

	if (!card)
		return -EINVAL;
#ifdef LINUX_2_1
	p = create_proc_entry(card->id, S_IFDIR | S_IRUGO | S_IXUGO, snd_proc_root);
	if (!p)
		return -ENOMEM;
	p->fill_inode = snd_info_fill_inode;
	card->proc_dir = p;
	sprintf(str, "%i", card->number);
	p = create_proc_entry(str, S_IFLNK | S_IRUGO | S_IWUGO | S_IXUGO, snd_proc_root);
	if (!p) {
		proc_unregister(snd_proc_root, card->proc_dir->low_ino);
		card->proc_dir = NULL;
		return -ENOMEM;
	}
	p->fill_inode = snd_info_fill_inode;
	p->ops = &snd_info_card_link_inode_operations;
	card->proc_dir_link = p;
#else
	card->proc_dir = (struct proc_dir_entry *)
				snd_malloc(sizeof(struct proc_dir_entry));
	if (!card->proc_dir) {
		return -ENOMEM;
	}
	*(card->proc_dir) = snd_info_proc_card_template;
	card->proc_dir->namelen = strlen(card->proc_dir->name = card->id);
	if (proc_register_dynamic(snd_proc_root, card->proc_dir) < 0) {
		snd_free(card->proc_dir, sizeof(struct proc_dir_entry));
		card->proc_dir = NULL;
		return -EAGAIN;
	}
	card->proc_dir_link = (struct proc_dir_entry *)
				snd_malloc(sizeof(struct proc_dir_entry));
	if (!card->proc_dir_link) {
		proc_unregister(snd_proc_root, card->proc_dir->low_ino);
		snd_free(card->proc_dir, sizeof(struct proc_dir_entry));
		card->proc_dir = NULL;
		return -ENOMEM;
	}
	*(card->proc_dir_link) = snd_info_proc_card_link_template;
	sprintf(str, "%i", card->number);
	card->proc_dir_link->name = snd_malloc(4);
	if (!card->proc_dir_link->name) {
		snd_free(card->proc_dir_link, sizeof(struct proc_dir_entry));
		proc_unregister(snd_proc_root, card->proc_dir->low_ino);
		snd_free(card->proc_dir, sizeof(struct proc_dir_entry));
		card->proc_dir_link = NULL;
		card->proc_dir = NULL;
		return -ENOMEM;
	}
	strcpy((char *) card->proc_dir_link->name, str);
	card->proc_dir_link->namelen = strlen(str);
	if (proc_register_dynamic(snd_proc_root, card->proc_dir_link) < 0) {
		snd_free(card->proc_dir_link, sizeof(struct proc_dir_entry));
		proc_unregister(snd_proc_root, card->proc_dir->low_ino);
		snd_free(card->proc_dir, sizeof(struct proc_dir_entry));
		card->proc_dir_link = NULL;
		card->proc_dir = NULL;
		return -EAGAIN;
	}
#endif
	return 0;
}

int snd_info_card_unregister(snd_card_t * card)
{
	if (!card)
		return -EINVAL;
	if (card->proc_dir_link) {
		proc_unregister(snd_proc_root, card->proc_dir_link->low_ino);
#ifndef LINUX_2_1
		snd_free((char *) card->proc_dir_link->name, 4);
		snd_free(card->proc_dir_link, sizeof(struct proc_dir_entry));
#endif
	}
	if (card->proc_dir) {
		proc_unregister(snd_proc_root, card->proc_dir->low_ino);
#ifndef LINUX_2_1
		snd_free(card->proc_dir, sizeof(struct proc_dir_entry));
#endif
	}
	card->proc_dir = NULL;
	return 0;
}

/*

 */

int snd_info_get_line(snd_info_buffer_t * buffer, char *line, int len)
{
	int c = -1;

	if (len <= 0 || buffer->stop || buffer->error)
		return 1;
	while (--len > 0) {
		c = *buffer->curr++;
		if (c == '\n') {
			if ((buffer->curr - buffer->buffer) >= buffer->size) {
				buffer->stop = 1;
			}
			break;
		}
		*line++ = c;
		if ((buffer->curr - buffer->buffer) >= buffer->size) {
			buffer->stop = 1;
			break;
		}
	}
	while (c != '\n' && !buffer->stop) {
		c = *buffer->curr++;
		if ((buffer->curr - buffer->buffer) >= buffer->size) {
			buffer->stop = 1;
		}
	}
	*line = '\0';
	return 0;
}

char *snd_info_get_str(char *dest, char *src, int len)
{
	int c;

	while (*src == ' ' || *src == '\t')
		src++;
	if (*src == '"' || *src == '\'') {
		c = *src++;
		while (--len > 0 && *src && *src != c) {
			*dest++ = *src++;
		}
		if (*src == c)
			src++;
	} else {
		while (--len > 0 && *src && *src != ' ' && *src != '\t') {
			*dest++ = *src++;
		}
	}
	*dest = 0;
	while (*src == ' ' || *src == '\t')
		src++;
	return src;
}

snd_info_entry_t *snd_info_create_entry(snd_card_t * card, const char *name)
{
	snd_info_entry_t *entry;

	entry = (snd_info_entry_t *) snd_calloc(sizeof(snd_info_entry_t));
	if (!entry)
		return NULL;
	entry->name = snd_malloc_strdup((char *) name);
	if (!entry->name) {
		snd_free(entry, sizeof(snd_info_entry_t));
		return NULL;
	}
	entry->mode = S_IFREG | S_IRUGO;
	entry->type = SND_INFO_ENTRY_TEXT;
	entry->card = card;
	if (card) {
		entry->use_inc = card->use_inc;
		entry->use_dec = card->use_dec;
	}
	snd_mutex_prepare(entry, access);
	return entry;
}

void snd_info_free_entry(snd_info_entry_t * entry)
{
	if (!entry)
		return;
	if (entry->name)
		snd_free_str((char *) entry->name);
	if (entry->private_free)
		entry->private_free(entry->private_data);
	snd_free(entry, sizeof(snd_info_entry_t));
}

#ifndef LINUX_2_1
static void snd_info_device_fill_inode(struct inode *inode)
{
	struct proc_dir_entry *de;
	snd_info_entry_t *entry;

	de = (struct proc_dir_entry *) inode->u.generic_ip;
	if (!de)
		return;
	entry = (snd_info_entry_t *) de->data;
	if (!entry)
		return;
	inode->i_rdev = MKDEV(entry->t.device.major, entry->t.device.minor);
}

static struct proc_dir_entry snd_info_device_template =
{
	0, 0, NULL,		/* inode, namelen, name */
	S_IFCHR | S_IRUGO | S_IWUGO, 1, 0, 0,	/* mode, nlink, uid, gid */
	0, &snd_info_device_inode_operations,	/* size, ops */
	NULL,			/* get_info */
	snd_info_device_fill_inode,	/* fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
#else
static void snd_info_device_fill_inode(struct inode *inode, int fill)
{
	struct proc_dir_entry *de;
	snd_info_entry_t *entry;

	if (!fill) {
		MOD_DEC_USE_COUNT;
		return;
	}
	MOD_INC_USE_COUNT;
	de = (struct proc_dir_entry *) inode->u.generic_ip;
	if (!de)
		return;
	entry = (snd_info_entry_t *) de->data;
	if (!entry)
		return;
	inode->i_rdev = MKDEV(entry->t.device.major, entry->t.device.minor);
}
#endif

snd_info_entry_t *snd_info_create_device(const char *name, unsigned int number, unsigned int mode)
{
	unsigned short major = number >> 16;
	unsigned short minor = (unsigned short) number;
	snd_info_entry_t *entry, *e;
	struct proc_dir_entry *p = NULL;

	if (!major)
		major = snd_major;
	if (!mode)
		mode = S_IFCHR | S_IRUGO | S_IWUGO;
	mode &= (snd_device_mode & (S_IRUGO | S_IWUGO)) | S_IFCHR | S_IFBLK;
	entry = snd_info_create_entry(NULL, name);
	if (!entry)
		return NULL;
	entry->type = SND_INFO_ENTRY_DEVICE;
	entry->mode = mode;
	entry->t.device.major = major;
	entry->t.device.minor = minor;
	snd_mutex_down_static(info);
#ifdef LINUX_2_1
	p = create_proc_entry(entry->name, entry->mode, snd_proc_dev);
	p->fill_inode = snd_info_device_fill_inode;
	if (p) {
		p->ops = &snd_info_device_inode_operations;
	} else {
		snd_mutex_up_static(info);
		snd_info_free_entry(entry);
		return NULL;
	}
#else
	p = (struct proc_dir_entry *) snd_malloc(sizeof(struct proc_dir_entry));
	if (!p) {
		snd_mutex_up_static(info);
		snd_info_free_entry(entry);
		return NULL;
	}
	*p = snd_info_device_template;
	p->namelen = strlen(p->name = entry->name);
	p->mode = entry->mode;
	if (proc_register_dynamic(snd_proc_dev, p) < 0) {
		snd_free(p, sizeof(struct proc_dir_entry));
		snd_mutex_up_static(info);
		snd_info_free_entry(entry);
		return NULL;
	}
#endif
	p->gid = snd_device_gid;
	p->uid = snd_device_uid;
	p->data = (void *) entry;
	entry->p = p;
	if (!(e = snd_info_devices)) {
		snd_info_devices = entry;
	} else {
		while (e->next)
			e = e->next;
		e->next = entry;
	}
	snd_mutex_up_static(info);
	return entry;
}

void snd_info_free_device(snd_info_entry_t * entry)
{
	snd_info_entry_t *e;

	if (!entry || !entry->p)
		return;
	snd_mutex_down_static(info);
	proc_unregister(snd_proc_dev, entry->p->low_ino);
	if (entry == (e = snd_info_devices)) {
		snd_info_devices = snd_info_devices->next;
	} else {
		while (e->next && e->next != entry)
			e = e->next;
		if (e->next)
			e->next = e->next->next;
	}
	snd_mutex_up_static(info);
#ifndef LINUX_2_1
	snd_free(entry->p, sizeof(struct proc_dir_entry));
#endif
	snd_info_free_entry(entry);
}

#ifndef LINUX_2_1
static struct proc_dir_entry snd_info_entry_template =
{
	0, 0, NULL,		/* inode, namelen, name */
	S_IFREG | S_IRUGO, 1, 0, 0,		/* mode, nlink, uid, gid */
	0, &snd_info_entry_inode_operations,	/* size, ops */
	NULL, NULL,		/* get_info, fill_inode */
	NULL,			/* next */
	NULL, NULL		/* parent, subdir */
};
#endif

static struct proc_dir_entry *snd_info_get_root(snd_info_entry_t * entry)
{
	struct proc_dir_entry *root;

	root = snd_proc_root;
	if (entry->card)
		root = entry->card->proc_dir;
	if (entry->type == SND_INFO_ENTRY_DATA &&
	    entry->subtype == SND_INFO_ENTRY_SDEVICE)
		root = snd_proc_dev;
	if (entry->subtype == SND_INFO_ENTRY_SEQUENCER)
		root = snd_proc_seq;
	return root;
}

int snd_info_register(snd_info_entry_t * entry)
{
	snd_info_entry_t *e;
	struct proc_dir_entry *root, *p = NULL;
	snd_card_t *card;
	int len;

	if (!entry)
		return -EINVAL;
	entry->next = NULL;
#if 0
	snd_printk("snd_info_register: %s, 0x%lx\n",
				entry->name, (long) entry->card);
#endif
	len = strlen(entry->name);
	card = entry->card;
	root = snd_info_get_root(entry);
	snd_mutex_down_static(info);
#ifdef LINUX_2_1
	p = create_proc_entry(entry->name, entry->mode, root);
	if (p) {
		p->fill_inode = snd_info_fill_inode;
		p->ops = &snd_info_entry_inode_operations;
		p->size = entry->size;
	} else {
		snd_mutex_up_static(info);
		return -ENOMEM;
	}
#else
	p = (struct proc_dir_entry *) snd_malloc(sizeof(struct proc_dir_entry));
	if (!p) {
		snd_mutex_up_static(info);
		return -ENOMEM;
	}
	*p = snd_info_entry_template;
	p->namelen = strlen(p->name = entry->name);
	p->mode = entry->mode;
	p->size = entry->size;
	if (proc_register_dynamic(root, p) < 0) {
		snd_free(p, sizeof(struct proc_dir_entry));
		snd_mutex_up_static(info);
		return -ENOMEM;
	}
#endif
	entry->p = p;
	if (!card) {
		if (!(e = snd_info_entries)) {
			snd_info_entries = entry;
		} else {
			while (e->next)
				e = e->next;
			e->next = entry;
		}
	} else {
		if (!(e = card->info_entries)) {
			card->info_entries = entry;
		} else {
			while (e->next)
				e = e->next;
			e->next = entry;
		}
	}
	snd_mutex_up_static(info);
	return 0;
}

int snd_info_unregister(snd_info_entry_t * entry)
{
	struct proc_dir_entry *root;
	snd_info_entry_t *e;

	if (!entry || !entry->p)
		return -EINVAL;
	root = snd_info_get_root(entry);
	snd_mutex_down_static(info);
	proc_unregister(root, entry->p->low_ino);
	if (!entry->card) {
		if (entry == (e = snd_info_entries)) {
			snd_info_entries = snd_info_entries->next;
		} else {
			while (e->next && e->next != entry)
				e = e->next;
			if (e->next)
				e->next = e->next->next;
		}
	} else {
		if (entry == (e = entry->card->info_entries)) {
			entry->card->info_entries = entry->card->info_entries->next;
		} else {
			while (e->next && e->next != entry)
				e = e->next;
			if (e->next)
				e->next = e->next->next;
		}
	}
	snd_mutex_up_static(info);
#ifndef LINUX_2_1
	snd_free(entry->p, sizeof(struct proc_dir_entry));
#endif
	snd_info_free_entry(entry);
	return 0;
}

/*

 */

static snd_info_entry_t *snd_info_version_entry = NULL;

static void snd_info_version_read(snd_info_buffer_t * buffer, void *private_data)
{
#ifdef MODULE_PARM
	static char *kernel_version = UTS_RELEASE;
#else
	extern char kernel_version[];
#endif

	snd_iprintf(buffer,
		    "Advanced Linux Sound Architecture Driver Version " CONFIG_SND_VERSION ".\n"
		    "Compiled in " __DATE__ " for kernel %s"
#ifdef __SMP__
		    " (SMP)"
#endif
#ifdef MODVERSIONS
		    " with versioned symbols"
#endif
		    ".\n", kernel_version);
}

static int snd_info_version_init(void)
{
	snd_info_entry_t *entry;

	entry = snd_info_create_entry(NULL, "version");
	if (!entry)
		return -ENOMEM;
	entry->t.text.read_size = 256;
	entry->t.text.read = snd_info_version_read;
	if (snd_info_register(entry) < 0) {
		snd_info_free_entry(entry);
		return -ENOMEM;
	}
	snd_info_version_entry = entry;
	return 0;
}

static int snd_info_version_done(void)
{
	if (snd_info_version_entry)
		snd_info_unregister(snd_info_version_entry);
	return 0;
}

/*
 *  store/restore settings via persistant module
 */

int snd_info_store_text(snd_info_entry_t * entry)
{
	int result = 0;
	snd_info_buffer_t ibuffer;
	char key[128];

	if (!entry)
		return -EINVAL;
	if (entry->type != SND_INFO_ENTRY_TEXT)
		return -EINVAL;
	memset(&ibuffer, 0, sizeof(ibuffer));
	ibuffer.len = (entry->t.text.read_size + (PAGE_SIZE - 1)) &
							 ~(PAGE_SIZE - 1);
	ibuffer.buffer = ibuffer.curr = vmalloc(ibuffer.len);
	if (!ibuffer.buffer)
		return -ENOMEM;
	snd_mutex_down(entry, access);
	entry->t.text.read(&ibuffer, entry->private_data);
	result = ibuffer.error;
	if (!result) {
		if (entry->card)
			sprintf(key, "snd:%i/%s", entry->card->number,
						  entry->name);
		else
			sprintf(key, "snd:%s", entry->name);
		if (ibuffer.size > 0) {
			result = snd_persist_store(key,
						   ibuffer.buffer,
						   ibuffer.size);
		} else {
			result = snd_persist_remove(key);
		}
	}
	snd_mutex_up(entry, access);
	vfree(ibuffer.buffer);
	return result;
}

int snd_info_restore_text(snd_info_entry_t * entry)
{
	int result = 0;
	snd_info_buffer_t ibuffer;
	char key[128];

	if (!entry)
		return -EINVAL;
	if (entry->type != SND_INFO_ENTRY_TEXT)
		return -EINVAL;
	memset(&ibuffer, 0, sizeof(ibuffer));
	ibuffer.len = (entry->t.text.write_size + (PAGE_SIZE - 1)) &
							~(PAGE_SIZE - 1);
	ibuffer.buffer = ibuffer.curr = vmalloc(ibuffer.len);
	if (!ibuffer.buffer)
		return -ENOMEM;
	snd_mutex_down(entry, access);
	if (entry->card)
		sprintf(key, "snd:%i/%s", entry->card->number, entry->name);
	else
		sprintf(key, "snd:%s", entry->name);
	result = snd_persist_restore(key, ibuffer.buffer, ibuffer.len);
	if (result > 0) {
		ibuffer.size = result;
		result = 0;
		entry->t.text.write(&ibuffer, entry->private_data);
		result = ibuffer.error;
	}
	snd_mutex_up(entry, access);
	vfree(ibuffer.buffer);
	return result;
}

#endif /* CONFIG_PROC_FS */
