/* net/atm/proc.c - ATM /proc interface */

/* Written 1995-2000 by Werner Almesberger, EPFL LRC/ICA */

/*
 * The mechanism used here isn't designed for speed but rather for convenience
 * of implementation. We only return one entry per read system call, so we can
 * be reasonably sure not to overrun the page and race conditions may lead to
 * the addition or omission of some lines but never to any corruption of a
 * line's internal structure.
 *
 * Making the whole thing slightly more efficient is left as an exercise to the
 * reader. (Suggestions: wrapper which loops to get several entries per system
 * call; or make --left slightly more clever to avoid O(n^2) characteristics.)
 * I find it fast enough on my unloaded 266 MHz Pentium 2 :-)
 */


#include <linux/config.h>
#include <linux/module.h> /* for EXPORT_SYMBOL */
#include <linux/string.h>
#include <linux/types.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/stat.h>
#include <linux/proc_fs.h>
#include <linux/errno.h>
#include <linux/atm.h>
#include <linux/atmdev.h>
#include <linux/netdevice.h>
#include <linux/atmclip.h>
#include <linux/atmarp.h>
#include <linux/if_arp.h>
#include <linux/init.h> /* for __init */
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <asm/param.h> /* for HZ */
#include "resources.h"
#include "common.h" /* atm_proc_init prototype */
#include "signaling.h" /* to get sigd - ugly too */

#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)
#include <net/atmclip.h>
#include "ipcommon.h"
#endif

#if defined(CONFIG_ATM_LANE) || defined(CONFIG_ATM_LANE_MODULE)
#include "lec.h"
#include "lec_arpc.h"
#endif

static ssize_t proc_dev_atm_read(struct file *file,char *buf,size_t count,
    loff_t *pos);
static ssize_t proc_spec_atm_read(struct file *file,char *buf,size_t count,
    loff_t *pos);

static struct file_operations proc_dev_atm_operations = {
	read:		proc_dev_atm_read,
};

static struct file_operations proc_spec_atm_operations = {
	read:		proc_spec_atm_read,
};

static void add_stats(char *buf,const char *aal,
  const struct k_atm_aal_stats *stats)
{
	sprintf(strchr(buf,0),"%s ( %d %d %d %d %d )",aal,
	    atomic_read(&stats->tx),atomic_read(&stats->tx_err),
	    atomic_read(&stats->rx),atomic_read(&stats->rx_err),
	    atomic_read(&stats->rx_drop));
}


static void dev_info(const struct atm_dev *dev,char *buf)
{
	int off,i;

	off = sprintf(buf,"%3d %-8s",dev->number,dev->type);
	for (i = 0; i < ESI_LEN; i++)
		off += sprintf(buf+off,"%02x",dev->esi[i]);
	strcat(buf,"  ");
	add_stats(buf,"0",&dev->stats.aal0);
	strcat(buf,"  ");
	add_stats(buf,"5",&dev->stats.aal5);
	sprintf(strchr(buf,0), "\t[%d]", atomic_read(&dev->refcnt));
	strcat(buf,"\n");
}


#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)

#define SEQ_NO_VCC_TOKEN   ((void *) 2)

static void svc_addr(struct seq_file *seq, struct sockaddr_atmsvc *addr)
{
	static int code[] = { 1,2,10,6,1,0 };
	static int e164[] = { 1,8,4,6,1,0 };

	if (*addr->sas_addr.pub) {
		seq_printf(seq, "%s", addr->sas_addr.pub);
		if (*addr->sas_addr.prv)
			seq_putc(seq, '+');
	} else if (!*addr->sas_addr.prv) {
		seq_printf(seq, "%s", "(none)");
		return;
	}
	if (*addr->sas_addr.prv) {
		unsigned char *prv = addr->sas_addr.prv;
		int *fields;
		int i, j;

		fields = *prv == ATM_AFI_E164 ? e164 : code;
		for (i = 0; fields[i]; i++) {
			for (j = fields[i]; j; j--)
				seq_printf(seq, "%02X", *prv++);
			if (fields[i+1]) 
				seq_putc(seq, '.');
		}
	}
}


static void atmarp_info(struct seq_file *seq, struct net_device *dev,struct
			atmarp_entry *entry, struct clip_vcc *clip_vcc) {
	unsigned long exp;
	char buf[17];
	int svc, llc, off;

	svc = ((clip_vcc == SEQ_NO_VCC_TOKEN) ||
	       (clip_vcc->vcc->sk->family == AF_ATMSVC));

	llc = ((clip_vcc == SEQ_NO_VCC_TOKEN) ||
	       (clip_vcc->encap));

	if (clip_vcc == SEQ_NO_VCC_TOKEN)
		exp = entry->neigh->used;
	else
		exp = clip_vcc->last_use;

	exp = (jiffies - exp) / HZ;

	seq_printf(seq, "%-6s%-4s%-4s%5ld ",
		   dev->name,
		   svc ? "SVC" : "PVC",
		   llc ? "LLC" : "NULL",
		   exp);

	off = snprintf(buf, sizeof(buf)-1, "%d.%d.%d.%d", NIPQUAD(entry->ip));
	while (off < 16)
		buf[off++] = ' ';
	buf[off] = '\0';
	seq_printf(seq, "%s", buf);

	if (clip_vcc == SEQ_NO_VCC_TOKEN) {
		if (time_before(jiffies, entry->expires))
			seq_printf(seq, "(resolving)\n");
		else
			seq_printf(seq, "(expired, ref %d)\n",
				   atomic_read(&entry->neigh->refcnt));
	} else if (!svc) {
		seq_printf(seq, "%d.%d.%d\n",
			   clip_vcc->vcc->dev->number,
			   clip_vcc->vcc->vpi,
			   clip_vcc->vcc->vci);
	} else {
		svc_addr(seq, &clip_vcc->vcc->remote);
		seq_putc(seq, '\n');
	}
}

struct clip_seq_state {
	/* This member must be first. */
	struct neigh_seq_state ns;

	/* Local to clip specific iteration. */
	struct clip_vcc *vcc;
};

static struct clip_vcc *clip_seq_next_vcc(struct atmarp_entry *e,
					  struct clip_vcc *curr)
{
	if (!curr) {
		curr = e->vccs;
		if (!curr)
			return SEQ_NO_VCC_TOKEN;
		return curr;
	}

	if (curr == SEQ_NO_VCC_TOKEN)
		return NULL;

	curr = curr->next;

	return curr;
}

static void *clip_seq_vcc_walk(struct clip_seq_state *state,
			       struct atmarp_entry *e, loff_t *pos)
{
	struct clip_vcc *vcc = state->vcc;

	vcc = clip_seq_next_vcc(e, vcc);
	if (vcc && pos != NULL) {
		while (*pos) {
			vcc = clip_seq_next_vcc(e, vcc);
			if (!vcc)
				break;
			--(*pos);
		}
	}
	state->vcc = vcc;

	return vcc;
}

static void *clip_seq_sub_iter(struct neigh_seq_state *_state,
			       struct neighbour *n, loff_t *pos)
{
	struct clip_seq_state *state = (struct clip_seq_state *) _state;

	return clip_seq_vcc_walk(state, NEIGH2ENTRY(n), pos);
}

static void *clip_seq_start(struct seq_file *seq, loff_t *pos)
{
	return neigh_seq_start(seq, pos, clip_tbl_hook, NEIGH_SEQ_NEIGH_ONLY);
}

static int clip_seq_show(struct seq_file *seq, void *v)
{
	static char atm_arp_banner[] = 
		"IPitf TypeEncp Idle IP address      ATM address\n";

	if (v == SEQ_START_TOKEN) {
		seq_puts(seq, atm_arp_banner);
	} else {
		struct clip_seq_state *state = seq->private;
		struct neighbour *n = v;
		struct clip_vcc *vcc = state->vcc;

		atmarp_info(seq, n->dev, NEIGH2ENTRY(n), vcc);
	}
	return 0;
}

static struct seq_operations arp_seq_ops = {
	.start	= clip_seq_start,
	.next	= neigh_seq_next,
	.stop	= neigh_seq_stop,
	.show	= clip_seq_show,
};

static int arp_seq_open(struct inode *inode, struct file *file)
{
	struct clip_seq_state *state;
	struct seq_file *seq;
	int rc = -EAGAIN;

	if (!clip_tbl_hook)
		goto out;

	state = kmalloc(sizeof(*state), GFP_KERNEL);
	if (!state) {
		rc = -ENOMEM;
		goto out_kfree;
	}
	memset(state, 0, sizeof(*state));
	state->ns.neigh_sub_iter = clip_seq_sub_iter;

	rc = seq_open(file, &arp_seq_ops);
	if (rc)
		goto out_kfree;

	seq = file->private_data;
	seq->private = state;
out:
	return rc;

out_kfree:
	kfree(state);
	goto out;
}

static struct file_operations arp_seq_fops = {
	.open		= arp_seq_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= seq_release_private,
	.owner		= THIS_MODULE,
};
#endif


static void pvc_info(struct atm_vcc *vcc, char *buf, int clip_info)
{
	static const char *class_name[] = { "off","UBR","CBR","VBR","ABR" };
	static const char *aal_name[] = {
		"---",	"1",	"2",	"3/4",	/*  0- 3 */
		"???",	"5",	"???",	"???",	/*  4- 7 */
		"???",	"???",	"???",	"???",	/*  8-11 */
		"???",	"0",	"???",	"???"};	/* 12-15 */
	int off;

	off = sprintf(buf,"%3d %3d %5d %-3s %7d %-5s %7d %-6s",
	    vcc->dev->number,vcc->vpi,vcc->vci,
	    vcc->qos.aal >= sizeof(aal_name)/sizeof(aal_name[0]) ? "err" :
	    aal_name[vcc->qos.aal],vcc->qos.rxtp.min_pcr,
	    class_name[vcc->qos.rxtp.traffic_class],vcc->qos.txtp.min_pcr,
	    class_name[vcc->qos.txtp.traffic_class]);
#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)
	if (clip_info && (vcc->push == atm_clip_ops->clip_push)) {
		struct clip_vcc *clip_vcc = CLIP_VCC(vcc);
		struct net_device *dev;

		dev = clip_vcc->entry ? clip_vcc->entry->neigh->dev : NULL;
		off += sprintf(buf+off,"CLIP, Itf:%s, Encap:",
		    dev ? dev->name : "none?");
		if (clip_vcc->encap)
			off += sprintf(buf+off,"LLC/SNAP");
		else
			off += sprintf(buf+off,"None");
	}
#endif
	strcpy(buf+off,"\n");
}


static const char *vcc_state(struct atm_vcc *vcc)
{
	static const char *map[] = { ATM_VS2TXT_MAP };

	return map[ATM_VF2VS(vcc->flags)];
}


static void vc_info(struct atm_vcc *vcc,char *buf)
{
	char *here;

	here = buf+sprintf(buf,"%p ",vcc);
	if (!vcc->dev) here += sprintf(here,"Unassigned    ");
	else here += sprintf(here,"%3d %3d %5d ",vcc->dev->number,vcc->vpi,
		    vcc->vci);
	switch (vcc->sk->family) {
		case AF_ATMPVC:
			here += sprintf(here,"PVC");
			break;
		case AF_ATMSVC:
			here += sprintf(here,"SVC");
			break;
		default:
			here += sprintf(here,"%3d",vcc->sk->family);
	}
	here += sprintf(here," %04lx  %5d %7d/%7d %7d/%7d\n",vcc->flags.bits,
	    vcc->reply,
	    atomic_read(&vcc->sk->wmem_alloc),vcc->sk->sndbuf,
	    atomic_read(&vcc->sk->rmem_alloc),vcc->sk->rcvbuf);
}


static void svc_info(struct atm_vcc *vcc,char *buf)
{
	char *here;
	int i;

	if (!vcc->dev)
		sprintf(buf,sizeof(void *) == 4 ? "N/A@%p%10s" : "N/A@%p%2s",
		    vcc,"");
	else sprintf(buf,"%3d %3d %5d         ",vcc->dev->number,vcc->vpi,
		    vcc->vci);
	here = strchr(buf,0);
	here += sprintf(here,"%-10s ",vcc_state(vcc));
	here += sprintf(here,"%s%s",vcc->remote.sas_addr.pub,
	    *vcc->remote.sas_addr.pub && *vcc->remote.sas_addr.prv ? "+" : "");
	if (*vcc->remote.sas_addr.prv)
		for (i = 0; i < ATM_ESA_LEN; i++)
			here += sprintf(here,"%02x",
			    vcc->remote.sas_addr.prv[i]);
	strcat(here,"\n");
}


#if defined(CONFIG_ATM_LANE) || defined(CONFIG_ATM_LANE_MODULE)

static char*
lec_arp_get_status_string(unsigned char status)
{
  switch(status) {
  case ESI_UNKNOWN:
    return "ESI_UNKNOWN       ";
  case ESI_ARP_PENDING:
    return "ESI_ARP_PENDING   ";
  case ESI_VC_PENDING:
    return "ESI_VC_PENDING    ";
  case ESI_FLUSH_PENDING:
    return "ESI_FLUSH_PENDING ";
  case ESI_FORWARD_DIRECT:
    return "ESI_FORWARD_DIRECT";
  default:
    return "<Unknown>         ";
  }
}

static void 
lec_info(struct lec_arp_table *entry, char *buf)
{
        int j, offset=0;

        for(j=0;j<ETH_ALEN;j++) {
                offset+=sprintf(buf+offset,"%2.2x",0xff&entry->mac_addr[j]);
        }
        offset+=sprintf(buf+offset, " ");
        for(j=0;j<ATM_ESA_LEN;j++) {
                offset+=sprintf(buf+offset,"%2.2x",0xff&entry->atm_addr[j]);
        }
        offset+=sprintf(buf+offset, " %s %4.4x",
                        lec_arp_get_status_string(entry->status),
                        entry->flags&0xffff);
        if (entry->vcc) {
                offset+=sprintf(buf+offset, "%3d %3d ", entry->vcc->vpi, 
                                entry->vcc->vci);                
        } else
                offset+=sprintf(buf+offset, "        ");
        if (entry->recv_vcc) {
                offset+=sprintf(buf+offset, "     %3d %3d", 
                                entry->recv_vcc->vpi, entry->recv_vcc->vci);
        }

        sprintf(buf+offset,"\n");
}

#endif

static int atm_devices_info(loff_t pos,char *buf)
{
	struct atm_dev *dev;
	struct list_head *p;
	int left;

	if (!pos) {
		return sprintf(buf,"Itf Type    ESI/\"MAC\"addr "
		    "AAL(TX,err,RX,err,drop) ...               [refcnt]\n");
	}
	left = pos-1;
	spin_lock(&atm_dev_lock);
	list_for_each(p, &atm_devs) {
		dev = list_entry(p, struct atm_dev, dev_list);
		if (left-- == 0) {
			dev_info(dev,buf);
			spin_unlock(&atm_dev_lock);
			return strlen(buf);
		}
	}
	spin_unlock(&atm_dev_lock);
	return 0;
}

/*
 * FIXME: it isn't safe to walk the VCC list without turning off interrupts.
 * What is really needed is some lock on the devices. Ditto for ATMARP.
 */

static int atm_pvc_info(loff_t pos,char *buf)
{
	struct sock *s;
	struct atm_vcc *vcc;
	int left, clip_info = 0;

	if (!pos) {
		return sprintf(buf,"Itf VPI VCI   AAL RX(PCR,Class) "
		    "TX(PCR,Class)\n");
	}
	left = pos-1;
#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)
	if (try_atm_clip_ops())
		clip_info = 1;
#endif
	read_lock(&vcc_sklist_lock);
	for(s = vcc_sklist; s; s = s->next) {
		vcc = s->protinfo.af_atm;
		if (vcc->sk->family == PF_ATMPVC && vcc->dev && !left--) {
			pvc_info(vcc,buf,clip_info);
			read_unlock(&vcc_sklist_lock);
#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)
			if (clip_info && atm_clip_ops->owner)
				__MOD_DEC_USE_COUNT(atm_clip_ops->owner);
#endif
			return strlen(buf);
		}
	}
	read_unlock(&vcc_sklist_lock);
#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)
	if (clip_info && atm_clip_ops->owner)
			__MOD_DEC_USE_COUNT(atm_clip_ops->owner);
#endif
	return 0;
}


static int atm_vc_info(loff_t pos,char *buf)
{
	struct atm_vcc *vcc;
	struct sock *s;
	int left;

	if (!pos)
		return sprintf(buf,sizeof(void *) == 4 ? "%-8s%s" : "%-16s%s",
		    "Address"," Itf VPI VCI   Fam Flags Reply Send buffer"
		    "     Recv buffer\n");
	left = pos-1;
	read_lock(&vcc_sklist_lock);
	for(s = vcc_sklist; s; s = s->next) {
		vcc = s->protinfo.af_atm;
		if (!left--) {
			vc_info(vcc,buf);
			read_unlock(&vcc_sklist_lock);
			return strlen(buf);
		}
	}
	read_unlock(&vcc_sklist_lock);

	return 0;
}


static int atm_svc_info(loff_t pos,char *buf)
{
	struct sock *s;
	struct atm_vcc *vcc;
	int left;

	if (!pos)
		return sprintf(buf,"Itf VPI VCI           State      Remote\n");
	left = pos-1;
	read_lock(&vcc_sklist_lock);
	for(s = vcc_sklist; s; s = s->next) {
		vcc = s->protinfo.af_atm;
		if (vcc->sk->family == PF_ATMSVC && !left--) {
			svc_info(vcc,buf);
			read_unlock(&vcc_sklist_lock);
			return strlen(buf);
		}
	}
	read_unlock(&vcc_sklist_lock);

	return 0;
}

#if defined(CONFIG_ATM_LANE) || defined(CONFIG_ATM_LANE_MODULE)
static int atm_lec_info(loff_t pos,char *buf)
{
	unsigned long flags;
	struct lec_priv *priv;
	struct lec_arp_table *entry;
	int i, count, d, e;
	struct net_device *dev;

	if (!pos) {
		return sprintf(buf,"Itf  MAC          ATM destination"
		    "                          Status            Flags "
		    "VPI/VCI Recv VPI/VCI\n");
	}
	if (!try_atm_lane_ops())
		return 0; /* the lane module is not there yet */

	count = pos;
	for(d = 0; d < MAX_LEC_ITF; d++) {
		dev = atm_lane_ops->get_lec(d);
		if (!dev || !(priv = (struct lec_priv *) dev->priv))
			continue;
		spin_lock_irqsave(&priv->lec_arp_lock, flags);
		for(i = 0; i < LEC_ARP_TABLE_SIZE; i++) {
			for(entry = priv->lec_arp_tables[i]; entry; entry = entry->next) {
				if (--count)
					continue;
				e = sprintf(buf,"%s ", dev->name);
				lec_info(entry, buf+e);
				spin_unlock_irqrestore(&priv->lec_arp_lock, flags);
				dev_put(dev);
				if (atm_lane_ops->owner)
					__MOD_DEC_USE_COUNT(atm_lane_ops->owner);
				return strlen(buf);
			}
		}
		for(entry = priv->lec_arp_empty_ones; entry; entry = entry->next) {
			if (--count)
				continue;
			e = sprintf(buf,"%s ", dev->name);
			lec_info(entry, buf+e);
			spin_unlock_irqrestore(&priv->lec_arp_lock, flags);
			dev_put(dev);
			if (atm_lane_ops->owner)
				__MOD_DEC_USE_COUNT(atm_lane_ops->owner);
			return strlen(buf);
		}
		for(entry = priv->lec_no_forward; entry; entry=entry->next) {
			if (--count)
				continue;
			e = sprintf(buf,"%s ", dev->name);
			lec_info(entry, buf+e);
			spin_unlock_irqrestore(&priv->lec_arp_lock, flags);
			dev_put(dev);
			if (atm_lane_ops->owner)
				__MOD_DEC_USE_COUNT(atm_lane_ops->owner);
			return strlen(buf);
		}
		for(entry = priv->mcast_fwds; entry; entry = entry->next) {
			if (--count)
				continue;
			e = sprintf(buf,"%s ", dev->name);
			lec_info(entry, buf+e);
			spin_unlock_irqrestore(&priv->lec_arp_lock, flags);
			dev_put(dev);
			if (atm_lane_ops->owner)
				__MOD_DEC_USE_COUNT(atm_lane_ops->owner);
			return strlen(buf);
		}
		spin_unlock_irqrestore(&priv->lec_arp_lock, flags);
		dev_put(dev);
	}
	if (atm_lane_ops->owner)
		__MOD_DEC_USE_COUNT(atm_lane_ops->owner);
	return 0;
}
#endif


static ssize_t proc_dev_atm_read(struct file *file,char *buf,size_t count,
    loff_t *pos)
{
	struct atm_dev *dev;
	unsigned long page;
	int length;

	if (count == 0) return 0;
	page = get_free_page(GFP_KERNEL);
	if (!page) return -ENOMEM;
	dev = ((struct proc_dir_entry *) file->f_dentry->d_inode->u.generic_ip)
	    ->data;
	if (!dev->ops->proc_read)
		length = -EINVAL;
	else {
		length = dev->ops->proc_read(dev,pos,(char *) page);
		if (length > count) length = -EINVAL;
	}
	if (length >= 0) {
		if (copy_to_user(buf,(char *) page,length)) length = -EFAULT;
		(*pos)++;
	}
	free_page(page);
	return length;
}


static ssize_t proc_spec_atm_read(struct file *file,char *buf,size_t count,
    loff_t *pos)
{
	unsigned long page;
	int length;
	int (*info)(loff_t,char *);
	info = ((struct proc_dir_entry *) file->f_dentry->d_inode->u.generic_ip)
	    ->data;

	if (count == 0) return 0;
	page = get_free_page(GFP_KERNEL);
	if (!page) return -ENOMEM;
	length = (*info)(*pos,(char *) page);
	if (length > count) length = -EINVAL;
	if (length >= 0) {
		if (copy_to_user(buf,(char *) page,length)) length = -EFAULT;
		(*pos)++;
	}
	free_page(page);
	return length;
}


struct proc_dir_entry *atm_proc_root;
EXPORT_SYMBOL(atm_proc_root);


int atm_proc_dev_register(struct atm_dev *dev)
{
	int digits,num;
	int error;

	error = -ENOMEM;
	digits = 0;
	for (num = dev->number; num; num /= 10) digits++;
	if (!digits) digits++;

	dev->proc_name = kmalloc(strlen(dev->type) + digits + 2, GFP_ATOMIC);
	if (!dev->proc_name)
		goto fail1;
	sprintf(dev->proc_name,"%s:%d",dev->type, dev->number);

	dev->proc_entry = create_proc_entry(dev->proc_name, 0, atm_proc_root);
	if (!dev->proc_entry)
		goto fail0;
	dev->proc_entry->data = dev;
	dev->proc_entry->proc_fops = &proc_dev_atm_operations;
	dev->proc_entry->owner = THIS_MODULE;
	return 0;
fail0:
	kfree(dev->proc_name);
fail1:
	return error;
}


void atm_proc_dev_deregister(struct atm_dev *dev)
{
	remove_proc_entry(dev->proc_name, atm_proc_root);
	kfree(dev->proc_name);
}


#define CREATE_ENTRY(name) \
    name = create_proc_entry(#name,0,atm_proc_root); \
    if (!name) goto cleanup; \
    name->data = atm_##name##_info; \
    name->proc_fops = &proc_spec_atm_operations; \
    name->owner = THIS_MODULE

static struct proc_dir_entry *devices = NULL, *pvc = NULL,
		*svc = NULL, *arp = NULL, *lec = NULL, *vc = NULL;

static void atm_proc_cleanup(void)
{
	if (devices)
		remove_proc_entry("devices",atm_proc_root);
	if (pvc)
		remove_proc_entry("pvc",atm_proc_root);
	if (svc)
		remove_proc_entry("svc",atm_proc_root);
	if (arp)
		remove_proc_entry("arp",atm_proc_root);
	if (lec)
		remove_proc_entry("lec",atm_proc_root);
	if (vc)
		remove_proc_entry("vc",atm_proc_root);
	remove_proc_entry("net/atm",NULL);
}

int atm_proc_init(void)
{
	atm_proc_root = proc_mkdir("net/atm",NULL);
	if (!atm_proc_root)
		return -ENOMEM;
	CREATE_ENTRY(devices);
	CREATE_ENTRY(pvc);
	CREATE_ENTRY(svc);
	CREATE_ENTRY(vc);
#if defined(CONFIG_ATM_CLIP) || defined(CONFIG_ATM_CLIP_MODULE)
	arp  = create_proc_entry("arp", S_IRUGO, atm_proc_root);
	if (!arp)
		goto cleanup;
	arp->proc_fops = &arp_seq_fops;
#endif
#if defined(CONFIG_ATM_LANE) || defined(CONFIG_ATM_LANE_MODULE)
	CREATE_ENTRY(lec);
#endif
	return 0;

cleanup:
	atm_proc_cleanup();
	return -ENOMEM;
}

void atm_proc_exit(void)
{
	atm_proc_cleanup();
}
