Commit Diff


commit - 7763a61a3582ef330bca54f225e8ec5325fbd35e
commit + b8c14089d8f4be73a908f82f62fce80ed2c14a8d
blob - /dev/null
blob + 6f95a23a8ab6b9d354f809fe41ca29270e2c510e (mode 644)
--- /dev/null
+++ src/cmd/plumb/fsys.c
@@ -0,0 +1,975 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <regexp.h>
+#include <thread.h>
+#include <auth.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "plumber.h"
+
+enum
+{
+	Stack = 8*1024
+};
+
+typedef struct Dirtab Dirtab;
+typedef struct Fid Fid;
+typedef struct Holdq Holdq;
+typedef struct Readreq Readreq;
+typedef struct Sendreq Sendreq;
+
+struct Dirtab
+{
+	char		*name;
+	uchar	type;
+	uint		qid;
+	uint		perm;
+	int		nopen;		/* #fids open on this port */
+	Fid		*fopen;
+	Holdq	*holdq;
+	Readreq	*readq;
+	Sendreq	*sendq;
+};
+
+struct Fid
+{
+	int		fid;
+	int		busy;
+	int		open;
+	int		mode;
+	Qid		qid;
+	Dirtab	*dir;
+	long		offset;		/* zeroed at beginning of each message, read or write */
+	char		*writebuf;		/* partial message written so far; offset tells how much */
+	Fid		*next;
+	Fid		*nextopen;
+};
+
+struct Readreq
+{
+	Fid		*fid;
+	Fcall		*fcall;
+	uchar	*buf;
+	Readreq	*next;
+};
+
+struct Sendreq
+{
+	int			nfid;		/* number of fids that should receive this message */
+	int			nleft;		/* number left that haven't received it */
+	Fid			**fid;	/* fid[nfid] */
+	Plumbmsg	*msg;
+	char			*pack;	/* plumbpack()ed message */
+	int			npack;	/* length of pack */
+	Sendreq		*next;
+};
+
+struct Holdq
+{
+	Plumbmsg	*msg;
+	Holdq		*next;
+};
+
+struct	/* needed because incref() doesn't return value */
+{
+	Lock;
+	int			ref;
+} rulesref;
+
+enum
+{
+	DEBUG	= 0,
+	NDIR	= 50,
+	Nhash	= 16,
+
+	Qdir		= 0,
+	Qrules	= 1,
+	Qsend	= 2,
+	Qport	= 3,
+	NQID	= Qport
+};
+
+static Dirtab dir[NDIR] =
+{
+	{ ".",			QTDIR,	Qdir,			0500|DMDIR },
+	{ "rules",		QTFILE,	Qrules,		0600 },
+	{ "send",		QTFILE,	Qsend,		0200 },
+};
+static int	ndir = NQID;
+
+static int		srvfd;
+static int		srvclosefd;			/* rock for end of pipe to close */
+static int		clockfd;
+static int		clock;
+static Fid		*fids[Nhash];
+static QLock	readlock;
+static QLock	queue;
+static char	srvfile[128];
+static int		messagesize = 8192+IOHDRSZ;	/* good start */
+
+static void	fsysproc(void*);
+static void fsysrespond(Fcall*, uchar*, char*);
+static Fid*	newfid(int);
+
+static Fcall* fsysflush(Fcall*, uchar*, Fid*);
+static Fcall* fsysversion(Fcall*, uchar*, Fid*);
+static Fcall* fsysauth(Fcall*, uchar*, Fid*);
+static Fcall* fsysattach(Fcall*, uchar*, Fid*);
+static Fcall* fsyswalk(Fcall*, uchar*, Fid*);
+static Fcall* fsysopen(Fcall*, uchar*, Fid*);
+static Fcall* fsyscreate(Fcall*, uchar*, Fid*);
+static Fcall* fsysread(Fcall*, uchar*, Fid*);
+static Fcall* fsyswrite(Fcall*, uchar*, Fid*);
+static Fcall* fsysclunk(Fcall*, uchar*, Fid*);
+static Fcall* fsysremove(Fcall*, uchar*, Fid*);
+static Fcall* fsysstat(Fcall*, uchar*, Fid*);
+static Fcall* fsyswstat(Fcall*, uchar*, Fid*);
+
+Fcall* 	(*fcall[Tmax])(Fcall*, uchar*, Fid*) =
+{
+	[Tflush]	= fsysflush,
+	[Tversion]	= fsysversion,
+	[Tauth]	= fsysauth,
+	[Tattach]	= fsysattach,
+	[Twalk]	= fsyswalk,
+	[Topen]	= fsysopen,
+	[Tcreate]	= fsyscreate,
+	[Tread]	= fsysread,
+	[Twrite]	= fsyswrite,
+	[Tclunk]	= fsysclunk,
+	[Tremove]= fsysremove,
+	[Tstat]	= fsysstat,
+	[Twstat]	= fsyswstat,
+};
+
+char	Ebadfcall[] =	"bad fcall type";
+char	Eperm[] = 	"permission denied";
+char	Enomem[] =	"malloc failed for buffer";
+char	Enotdir[] =	"not a directory";
+char	Enoexist[] =	"plumb file does not exist";
+char	Eisdir[] =		"file is a directory";
+char	Ebadmsg[] =	"bad plumb message format";
+char Enosuchport[] ="no such plumb port";
+char Enoport[] =	"couldn't find destination for message";
+char	Einuse[] = 	"file already open";
+
+/*
+ * Add new port.  A no-op if port already exists or is the null string
+ */
+void
+addport(char *port)
+{
+	int i;
+
+	if(port == nil)
+		return;
+	for(i=NQID; i<ndir; i++)
+		if(strcmp(port, dir[i].name) == 0)
+			return;
+	if(i == NDIR){
+		fprint(2, "plumb: too many ports; max %d\n", NDIR);
+		return;
+	}
+	ndir++;
+	dir[i].name = estrdup(port);
+	dir[i].qid = i;
+	dir[i].perm = 0400;
+	nports++;
+	ports = erealloc(ports, nports*sizeof(char*));
+	ports[nports-1] = dir[i].name;
+}
+
+static ulong
+getclock(void)
+{
+	char buf[32];
+
+	seek(clockfd, 0, 0);
+	read(clockfd, buf, sizeof buf);
+	return atoi(buf);
+}
+
+void
+startfsys(void)
+{
+	int p[2], fd;
+
+	fmtinstall('F', fcallfmt);
+	clockfd = open("/dev/time", OREAD|OCEXEC);
+	clock = getclock();
+	if(pipe(p) < 0)
+		error("can't create pipe: %r");
+	/* 0 will be server end, 1 will be client end */
+	srvfd = p[0];
+	srvclosefd = p[1];
+	sprint(srvfile, "/srv/plumb.%s.%d", user, getpid());
+	if(putenv("plumbsrv", srvfile) < 0)
+		error("can't write $plumbsrv: %r");
+	fd = create(srvfile, OWRITE|OCEXEC|ORCLOSE, 0600);
+	if(fd < 0)
+		error("can't create /srv file: %r");
+	if(fprint(fd, "%d", p[1]) <= 0)
+		error("can't write /srv/file: %r");
+	/* leave fd open; ORCLOSE will take care of it */
+
+	procrfork(fsysproc, nil, Stack, RFFDG);
+
+	close(p[0]);
+	if(mount(p[1], -1, "/mnt/plumb", MREPL, "") < 0)
+		error("can't mount /mnt/plumb: %r");
+	close(p[1]);
+}
+
+static void
+fsysproc(void*)
+{
+	int n;
+	Fcall *t;
+	Fid *f;
+	uchar *buf;
+
+	close(srvclosefd);
+	srvclosefd = -1;
+	t = nil;
+	for(;;){
+		buf = malloc(messagesize);	/* avoid memset of emalloc */
+		if(buf == nil)
+			error("malloc failed: %r");
+		qlock(&readlock);
+		n = read9pmsg(srvfd, buf, messagesize);
+		if(n <= 0){
+			if(n < 0)
+				error("i/o error on server channel");
+			threadexitsall("unmounted");
+		}
+		if(readlock.head == nil)	/* no other processes waiting to read; start one */
+			proccreate(fsysproc, nil, Stack);
+		qunlock(&readlock);
+		if(t == nil)
+			t = emalloc(sizeof(Fcall));
+		if(convM2S(buf, n, t) != n)
+			error("convert error in convM2S");
+		if(DEBUG)
+			fprint(2, "<= %F\n", t);
+		if(fcall[t->type] == nil)
+			fsysrespond(t, buf, Ebadfcall);
+		else{
+			if(t->type==Tversion || t->type==Tauth)
+				f = nil;
+			else
+				f = newfid(t->fid);
+			t = (*fcall[t->type])(t, buf, f);
+		}
+	}
+}
+
+static void
+fsysrespond(Fcall *t, uchar *buf, char *err)
+{
+	int n;
+
+	if(err){
+		t->type = Rerror;
+		t->ename = err;
+	}else
+		t->type++;
+	if(buf == nil)
+		buf = emalloc(messagesize);
+	n = convS2M(t, buf, messagesize);
+	if(n < 0)
+		error("convert error in convS2M");
+	if(write(srvfd, buf, n) != n)
+		error("write error in respond");
+	if(DEBUG)
+		fprint(2, "=> %F\n", t);
+	free(buf);
+}
+
+static
+Fid*
+newfid(int fid)
+{
+	Fid *f, *ff, **fh;
+
+	qlock(&queue);
+	ff = nil;
+	fh = &fids[fid&(Nhash-1)];
+	for(f=*fh; f; f=f->next)
+		if(f->fid == fid)
+			goto Return;
+		else if(ff==nil && !f->busy)
+			ff = f;
+	if(ff){
+		ff->fid = fid;
+		f = ff;
+		goto Return;
+	}
+	f = emalloc(sizeof *f);
+	f->fid = fid;
+	f->next = *fh;
+	*fh = f;
+    Return:
+	qunlock(&queue);
+	return f;
+}
+
+static uint
+dostat(Dirtab *dir, uchar *buf, uint nbuf, uint clock)
+{
+	Dir d;
+
+	d.qid.type = dir->type;
+	d.qid.path = dir->qid;
+	d.qid.vers = 0;
+	d.mode = dir->perm;
+	d.length = 0;	/* would be nice to do better */
+	d.name = dir->name;
+	d.uid = user;
+	d.gid = user;
+	d.muid = user;
+	d.atime = clock;
+	d.mtime = clock;
+	return convD2M(&d, buf, nbuf);
+}
+
+static void
+queuesend(Dirtab *d, Plumbmsg *m)
+{
+	Sendreq *s, *t;
+	Fid *f;
+	int i;
+
+	s = emalloc(sizeof(Sendreq));
+	s->nfid = d->nopen;
+	s->nleft = s->nfid;
+	s->fid = emalloc(s->nfid*sizeof(Fid*));
+	i = 0;
+	/* build array of fids open on this channel */
+	for(f=d->fopen; f!=nil; f=f->nextopen)
+		s->fid[i++] = f;
+	s->msg = m;
+	s->next = nil;
+	/* link to end of queue; drainqueue() searches in sender order so this implements a FIFO */
+	for(t=d->sendq; t!=nil; t=t->next)
+		if(t->next == nil)
+			break;
+	if(t == nil)
+		d->sendq = s;
+	else
+		t->next = s;
+}
+
+static void
+queueread(Dirtab *d, Fcall *t, uchar *buf, Fid *f)
+{
+	Readreq *r;
+
+	r = emalloc(sizeof(Readreq));
+	r->fcall = t;
+	r->buf = buf;
+	r->fid = f;
+	r->next = d->readq;
+	d->readq = r;
+}
+
+static void
+drainqueue(Dirtab *d)
+{
+	Readreq *r, *nextr, *prevr;
+	Sendreq *s, *nexts, *prevs;
+	int i, n;
+
+	prevs = nil;
+	for(s=d->sendq; s!=nil; s=nexts){
+		nexts = s->next;
+		for(i=0; i<s->nfid; i++){
+			prevr = nil;
+			for(r=d->readq; r!=nil; r=nextr){
+				nextr = r->next;
+				if(r->fid == s->fid[i]){
+					/* pack the message if necessary */
+					if(s->pack == nil)
+						s->pack = plumbpack(s->msg, &s->npack);
+					/* exchange the stuff... */
+					r->fcall->data = s->pack+r->fid->offset;
+					n = s->npack - r->fid->offset;
+					if(n > messagesize-IOHDRSZ)
+						n = messagesize-IOHDRSZ;
+					if(n > r->fcall->count)
+						n = r->fcall->count;
+					r->fcall->count = n;
+					fsysrespond(r->fcall, r->buf, nil);
+					r->fid->offset += n;
+					if(r->fid->offset >= s->npack){
+						/* message transferred; delete this fid from send queue */
+						r->fid->offset = 0;
+						s->fid[i] = nil;
+						s->nleft--;
+					}
+					/* delete read request from queue */
+					if(prevr)
+						prevr->next = r->next;
+					else
+						d->readq = r->next;
+					free(r->fcall);
+					free(r);
+					break;
+				}else
+					prevr = r;
+			}
+		}
+		/* if no fids left, delete this send from queue */
+		if(s->nleft == 0){
+			free(s->fid);
+			plumbfree(s->msg);
+			free(s->pack);
+			if(prevs)
+				prevs->next = s->next;
+			else
+				d->sendq = s->next;
+			free(s);
+		}else
+			prevs = s;
+	}
+}
+
+/* can't flush a send because they are always answered synchronously */
+static void
+flushqueue(Dirtab *d, int oldtag)
+{
+	Readreq *r, *prevr;
+
+	prevr = nil;
+	for(r=d->readq; r!=nil; r=r->next){
+		if(oldtag == r->fcall->tag){
+			/* delete read request from queue */
+			if(prevr)
+				prevr->next = r->next;
+			else
+				d->readq = r->next;
+			free(r->fcall);
+			free(r->buf);
+			free(r);
+			return;
+		}
+		prevr = r;
+	}
+}
+
+/* remove messages awaiting delivery to now-closing fid */
+static void
+removesenders(Dirtab *d, Fid *fid)
+{
+	Sendreq *s, *nexts, *prevs;
+	int i;
+
+	prevs = nil;
+	for(s=d->sendq; s!=nil; s=nexts){
+		nexts = s->next;
+		for(i=0; i<s->nfid; i++)
+			if(fid == s->fid[i]){
+				/* delete this fid from send queue */
+				s->fid[i] = nil;
+				s->nleft--;
+				break;
+			}
+		/* if no fids left, delete this send from queue */
+		if(s->nleft == 0){
+			free(s->fid);
+			plumbfree(s->msg);
+			free(s->pack);
+			if(prevs)
+				prevs->next = s->next;
+			else
+				d->sendq = s->next;
+			free(s);
+		}else
+			prevs = s;
+	}
+}
+
+static void
+hold(Plumbmsg *m, Dirtab *d)
+{
+	Holdq *h, *q;
+
+	h = emalloc(sizeof(Holdq));
+	h->msg = m;
+	/* add to end of queue */
+	if(d->holdq == nil)
+		d->holdq = h;
+	else{
+		for(q=d->holdq; q->next!=nil; q=q->next)
+			;
+		q->next = h;
+	}
+}
+
+static void
+queueheld(Dirtab *d)
+{
+	Holdq *h;
+
+	while(d->holdq != nil){
+		h = d->holdq;
+		d->holdq = h->next;
+		queuesend(d, h->msg);
+		/* no need to drain queue because we know no-one is reading yet */
+		free(h);
+	}
+}
+
+static void
+dispose(Fcall *t, uchar *buf, Plumbmsg *m, Ruleset *rs, Exec *e)
+{
+	int i;
+	char *err;
+
+	qlock(&queue);
+	err = nil;
+	if(m->dst==nil || m->dst[0]=='\0'){
+		err = Enoport;
+		if(rs != nil)
+			err = startup(rs, e);
+		plumbfree(m);
+	}else
+		for(i=NQID; i<ndir; i++)
+			if(strcmp(m->dst, dir[i].name) == 0){
+				if(dir[i].nopen == 0){
+					err = startup(rs, e);
+					if(e!=nil && e->holdforclient)
+						hold(m, &dir[i]);
+					else
+						plumbfree(m);
+				}else{
+					queuesend(&dir[i], m);
+					drainqueue(&dir[i]);
+				}
+				break;
+			}
+	freeexec(e);
+	qunlock(&queue);
+	fsysrespond(t, buf, err);
+	free(t);
+}
+
+static Fcall*
+fsysversion(Fcall *t, uchar *buf, Fid*)
+{
+	if(t->msize < 256){
+		fsysrespond(t, buf, "version: message size too small");
+		return t;
+	}
+	if(t->msize < messagesize)
+		messagesize = t->msize;
+	t->msize = messagesize;
+	if(strncmp(t->version, "9P2000", 6) != 0){
+		fsysrespond(t, buf, "unrecognized 9P version");
+		return t;
+	}
+	t->version = "9P2000";
+	fsysrespond(t, buf, nil);
+	return t;
+}
+
+static Fcall*
+fsysauth(Fcall *t, uchar *buf, Fid*)
+{
+	fsysrespond(t, buf, "plumber: authentication not required");
+	return t;
+}
+
+static Fcall*
+fsysattach(Fcall *t, uchar *buf, Fid *f)
+{
+	Fcall out;
+
+	if(strcmp(t->uname, user) != 0){
+		fsysrespond(&out, buf, Eperm);
+		return t;
+	}
+	f->busy = 1;
+	f->open = 0;
+	f->qid.type = QTDIR;
+	f->qid.path = Qdir;
+	f->qid.vers = 0;
+	f->dir = dir;
+	memset(&out, 0, sizeof(Fcall));
+	out.type = t->type;
+	out.tag = t->tag;
+	out.fid = f->fid;
+	out.qid = f->qid;
+	fsysrespond(&out, buf, nil);
+	return t;
+}
+
+static Fcall*
+fsysflush(Fcall *t, uchar *buf, Fid*)
+{
+	int i;
+
+	qlock(&queue);
+	for(i=NQID; i<ndir; i++)
+		flushqueue(&dir[i], t->oldtag);
+	qunlock(&queue);
+	fsysrespond(t, buf, nil);
+	return t;
+}
+
+static Fcall*
+fsyswalk(Fcall *t, uchar *buf, Fid *f)
+{
+	Fcall out;
+	Fid *nf;
+	ulong path;
+	Dirtab *d, *dir;
+	Qid q;
+	int i;
+	uchar type;
+	char *err;
+
+	if(f->open){
+		fsysrespond(t, buf, "clone of an open fid");
+		return t;
+	}
+
+	nf = nil;
+	if(t->fid  != t->newfid){
+		nf = newfid(t->newfid);
+		if(nf->busy){
+			fsysrespond(t, buf, "clone to a busy fid");
+			return t;
+		}
+		nf->busy = 1;
+		nf->open = 0;
+		nf->dir = f->dir;
+		nf->qid = f->qid;
+		f = nf;	/* walk f */
+	}
+
+	out.nwqid = 0;
+	err = nil;
+	dir = f->dir;
+	q = f->qid;
+
+	if(t->nwname > 0){
+		for(i=0; i<t->nwname; i++){
+			if((q.type & QTDIR) == 0){
+				err = Enotdir;
+				break;
+			}
+			if(strcmp(t->wname[i], "..") == 0){
+				type = QTDIR;
+				path = Qdir;
+	Accept:
+				q.type = type;
+				q.vers = 0;
+				q.path = path;
+				out.wqid[out.nwqid++] = q;
+				continue;
+			}
+			d = dir;
+			d++;	/* skip '.' */
+			for(; d->name; d++)
+				if(strcmp(t->wname[i], d->name) == 0){
+					type = d->type;
+					path = d->qid;
+					dir = d;
+					goto Accept;
+				}
+			err = Enoexist;
+			break;
+		}
+	}
+
+	out.type = t->type;
+	out.tag = t->tag;
+	if(err!=nil || out.nwqid<t->nwname){
+		if(nf)
+			nf->busy = 0;
+	}else if(out.nwqid == t->nwname){
+		f->qid = q;
+		f->dir = dir;
+	}
+
+	fsysrespond(&out, buf, err);
+	return t;
+}
+
+static Fcall*
+fsysopen(Fcall *t, uchar *buf, Fid *f)
+{
+	int m, clearrules, mode;
+
+	clearrules = 0;
+	if(t->mode & OTRUNC){
+		if(f->qid.path != Qrules)
+			goto Deny;
+		clearrules = 1;
+	}
+	/* can't truncate anything, so just disregard */
+	mode = t->mode & ~(OTRUNC|OCEXEC);
+	/* can't execute or remove anything */
+	if(mode==OEXEC || (mode&ORCLOSE))
+		goto Deny;
+	switch(mode){
+	default:
+		goto Deny;
+	case OREAD:
+		m = 0400;
+		break;
+	case OWRITE:
+		m = 0200;
+		break;
+	case ORDWR:
+		m = 0600;
+		break;
+	}
+	if(((f->dir->perm&~(DMDIR|DMAPPEND))&m) != m)
+		goto Deny;
+	if(f->qid.path==Qrules && (mode==OWRITE || mode==ORDWR)){
+		lock(&rulesref);
+		if(rulesref.ref++ != 0){
+			rulesref.ref--;
+			unlock(&rulesref);
+			fsysrespond(t, buf, Einuse);
+			return t;
+		}
+		unlock(&rulesref);
+	}
+	if(clearrules){
+		writerules(nil, 0);
+		rules[0] = nil;
+	}
+	t->qid = f->qid;
+	t->iounit = 0;
+	qlock(&queue);
+	f->mode = mode;
+	f->open = 1;
+	f->dir->nopen++;
+	f->nextopen = f->dir->fopen;
+	f->dir->fopen = f;
+	queueheld(f->dir);
+	qunlock(&queue);
+	fsysrespond(t, buf, nil);
+	return t;
+
+    Deny:
+	fsysrespond(t, buf, Eperm);
+	return t;
+}
+
+static Fcall*
+fsyscreate(Fcall *t, uchar *buf, Fid*)
+{
+	fsysrespond(t, buf, Eperm);
+	return t;
+}
+
+static Fcall*
+fsysreadrules(Fcall *t, uchar *buf)
+{
+	char *p;
+	int n;
+
+	p = printrules();
+	n = strlen(p);
+	t->data = p;
+	if(t->offset >= n)
+		t->count = 0;
+	else{
+		t->data = p+t->offset;
+		if(t->offset+t->count > n)
+			t->count = n-t->offset;
+	}
+	fsysrespond(t, buf, nil);
+	free(p);
+	return t;
+}
+
+static Fcall*
+fsysread(Fcall *t, uchar *buf, Fid *f)
+{
+	uchar *b;
+	int i, n, o, e;
+	uint len;
+	Dirtab *d;
+	uint clock;
+
+	if(f->qid.path != Qdir){
+		if(f->qid.path == Qrules)
+			return fsysreadrules(t, buf);
+		/* read from port */
+		if(f->qid.path < NQID){
+			fsysrespond(t, buf, "internal error: unknown read port");
+			return t;
+		}
+		qlock(&queue);
+		queueread(f->dir, t, buf, f);
+		drainqueue(f->dir);
+		qunlock(&queue);
+		return nil;
+	}
+	o = t->offset;
+	e = t->offset+t->count;
+	clock = getclock();
+	b = malloc(messagesize-IOHDRSZ);
+	if(b == nil){
+		fsysrespond(t, buf, Enomem);
+		return t;
+	}
+	n = 0;
+	d = dir;
+	d++;	/* first entry is '.' */
+	for(i=0; d->name!=nil && i<e; i+=len){
+		len = dostat(d, b+n, messagesize-IOHDRSZ-n, clock);
+		if(len <= BIT16SZ)
+			break;
+		if(i >= o)
+			n += len;
+		d++;
+	}
+	t->data = (char*)b;
+	t->count = n;
+	fsysrespond(t, buf, nil);
+	free(b);
+	return t;
+}
+
+static Fcall*
+fsyswrite(Fcall *t, uchar *buf, Fid *f)
+{
+	Plumbmsg *m;
+	int i, n;
+	long count;
+	char *data;
+	Exec *e;
+
+	switch((int)f->qid.path){
+	case Qdir:
+		fsysrespond(t, buf, Eisdir);
+		return t;
+	case Qrules:
+		clock = getclock();
+		fsysrespond(t, buf, writerules(t->data, t->count));
+		return t;
+	case Qsend:
+		if(f->offset == 0){
+			data = t->data;
+			count = t->count;
+		}else{
+			/* partial message already assembled */
+			f->writebuf = erealloc(f->writebuf, f->offset + t->count);
+			memmove(f->writebuf+f->offset, t->data, t->count);
+			data = f->writebuf;
+			count = f->offset+t->count;
+		}
+		m = plumbunpackpartial(data, count, &n);
+		if(m == nil){
+			if(n == 0){
+				f->offset = 0;
+				free(f->writebuf);
+				f->writebuf = nil;
+				fsysrespond(t, buf, Ebadmsg);
+				return t;
+			}
+			/* can read more... */
+			if(f->offset == 0){
+				f->writebuf = emalloc(t->count);
+				memmove(f->writebuf, t->data, t->count);
+			}
+			/* else buffer has already been grown */
+			f->offset += t->count;
+			fsysrespond(t, buf, nil);
+			return t;
+		}
+		/* release partial buffer */
+		f->offset = 0;
+		free(f->writebuf);
+		f->writebuf = nil;
+		for(i=0; rules[i]; i++)
+			if((e=matchruleset(m, rules[i])) != nil){
+				dispose(t, buf, m, rules[i], e);
+				return nil;
+			}
+		if(m->dst != nil){
+			dispose(t, buf, m, nil, nil);
+			return nil;
+		}
+		fsysrespond(t, buf, "no matching plumb rule");
+		return t;
+	}
+	fsysrespond(t, buf, "internal error: write to unknown file");
+	return t;
+}
+
+static Fcall*
+fsysstat(Fcall *t, uchar *buf, Fid *f)
+{
+	t->stat = emalloc(messagesize-IOHDRSZ);
+	t->nstat = dostat(f->dir, t->stat, messagesize-IOHDRSZ, clock);
+	fsysrespond(t, buf, nil);
+	free(t->stat);
+	t->stat = nil;
+	return t;
+}
+
+static Fcall*
+fsyswstat(Fcall *t, uchar *buf, Fid*)
+{
+	fsysrespond(t, buf, Eperm);
+	return t;
+}
+
+static Fcall*
+fsysremove(Fcall *t, uchar *buf, Fid*)
+{
+	fsysrespond(t, buf, Eperm);
+	return t;
+}
+
+static Fcall*
+fsysclunk(Fcall *t, uchar *buf, Fid *f)
+{
+	Fid *prev, *p;
+	Dirtab *d;
+
+	qlock(&queue);
+	if(f->open){
+		d = f->dir;
+		d->nopen--;
+		if(d->qid==Qrules && (f->mode==OWRITE || f->mode==ORDWR)){
+			/*
+			 * just to be sure last rule is parsed; error messages will be lost, though,
+			 * unless last write ended with a blank line
+			 */
+			writerules(nil, 0);
+			lock(&rulesref);
+			rulesref.ref--;
+			unlock(&rulesref);
+		}
+		prev = nil;
+		for(p=d->fopen; p; p=p->nextopen){
+			if(p == f){
+				if(prev)
+					prev->nextopen = f->nextopen;
+				else
+					d->fopen = f->nextopen;
+				removesenders(d, f);
+				break;
+			}
+			prev = p;
+		}
+	}
+	f->busy = 0;
+	f->open = 0;
+	f->offset = 0;
+	if(f->writebuf != nil){
+		free(f->writebuf);
+		f->writebuf = nil;
+	}
+	qunlock(&queue);
+	fsysrespond(t, buf, nil);
+	return t;
+}
blob - /dev/null
blob + 42a9232ff77a245681a9e294e09e2f18de8a7504 (mode 644)
--- /dev/null
+++ src/cmd/plumb/match.c
@@ -0,0 +1,463 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <regexp.h>
+#include <thread.h>
+#include <plumb.h>
+#include "plumber.h"
+
+static char*
+nonnil(char *s)
+{
+	if(s == nil)
+		return "";
+	return s;
+}
+
+int
+verbis(int obj, Plumbmsg *m, Rule *r)
+{
+	switch(obj){
+	default:
+		fprint(2, "unimplemented 'is' object %d\n", obj);
+		break;
+	case OData:
+		return strcmp(m->data, r->qarg) == 0;
+	case ODst:
+		return strcmp(m->dst, r->qarg) == 0;
+	case OType:
+		return strcmp(m->type, r->qarg) == 0;
+	case OWdir:
+		return strcmp(m->wdir, r->qarg) == 0;
+	case OSrc:
+		return strcmp(m->src, r->qarg) == 0;
+	}
+	return 0;
+}
+
+static void
+setvar(Resub rs[10], char *match[10])
+{
+	int i, n;
+
+	for(i=0; i<10; i++){
+		free(match[i]);
+		match[i] = nil;
+	}
+	for(i=0; i<10 && rs[i].sp!=nil; i++){
+		n = rs[i].ep-rs[i].sp;
+		match[i] = emalloc(n+1);
+		memmove(match[i], rs[i].sp, n);
+		match[i][n] = '\0';
+	}
+}
+
+int
+clickmatch(Reprog *re, char *text, Resub rs[10], int click)
+{
+	char *clickp;
+	int i, w;
+	Rune r;
+
+	/* click is in characters, not bytes */
+	for(i=0; i<click && text[i]!='\0'; i+=w)
+		w = chartorune(&r, text+i);
+	clickp = text+i;
+	for(i=0; i<=click; i++){
+		memset(rs, 0, 10*sizeof(Resub));
+		if(regexec(re, text+i, rs, 10))
+			if(rs[0].sp<=clickp && clickp<=rs[0].ep)
+				return 1;
+	}
+	return 0;
+}
+
+int
+verbmatches(int obj, Plumbmsg *m, Rule *r, Exec *e)
+{
+	Resub rs[10];
+	char *clickval, *alltext;
+	int p0, p1, ntext;
+
+	memset(rs, 0, sizeof rs);
+	ntext = -1;
+	switch(obj){
+	default:
+		fprint(2, "unimplemented 'matches' object %d\n", obj);
+		break;
+	case OData:
+		clickval = plumblookup(m->attr, "click");
+		if(clickval == nil){
+			alltext = m->data;
+			ntext = m->ndata;
+			goto caseAlltext;
+		}
+		if(!clickmatch(r->regex, m->data, rs, atoi(clickval)))
+			break;
+		p0 = rs[0].sp - m->data;
+		p1 = rs[0].ep - m->data;
+		if(e->p0 >=0 && !(p0==e->p0 && p1==e->p1))
+			break;
+		e->clearclick = 1;
+		e->setdata = 1;
+		e->p0 = p0;
+		e->p1 = p1;
+		setvar(rs, e->match);
+		return 1;
+	case ODst:
+		alltext = m->dst;
+		goto caseAlltext;
+	case OType:
+		alltext = m->type;
+		goto caseAlltext;
+	case OWdir:
+		alltext = m->wdir;
+		goto caseAlltext;
+	case OSrc:
+		alltext = m->src;
+		/* fall through */
+	caseAlltext:
+		/* must match full text */
+		if(ntext < 0)
+			ntext = strlen(alltext);
+		if(!regexec(r->regex, alltext, rs, 10) || rs[0].sp!=alltext || rs[0].ep!=alltext+ntext)
+			break;
+		setvar(rs, e->match);
+		return 1;
+	}
+	return 0;
+}
+
+int
+isfile(char *file, ulong maskon, ulong maskoff)
+{
+	Dir *d;
+	int mode;
+
+	d = dirstat(file);
+	if(d == nil)
+		return 0;
+	mode = d->mode;
+	free(d);
+	if((mode & maskon) == 0)
+		return 0;
+	if(mode & maskoff)
+		return 0;
+	return 1;
+}
+
+char*
+absolute(char *dir, char *file)
+{
+	char *p;
+
+	if(file[0] == '/')
+		return estrdup(file);
+	p = emalloc(strlen(dir)+1+strlen(file)+1);
+	sprint(p, "%s/%s", dir, file);
+	return cleanname(p);
+}
+
+int
+verbisfile(int obj, Plumbmsg *m, Rule *r, Exec *e, ulong maskon, ulong maskoff, char **var)
+{
+	char *file;
+
+	switch(obj){
+	default:
+		fprint(2, "unimplemented 'isfile' object %d\n", obj);
+		break;
+	case OArg:
+		file = absolute(m->wdir, expand(e, r->arg, nil));
+		if(isfile(file, maskon, maskoff)){
+			*var = file;
+			return 1;
+		}
+		free(file);
+		break;
+	case OData:
+	case OWdir:
+		file = absolute(m->wdir, obj==OData? m->data : m->wdir);
+		if(isfile(file, maskon, maskoff)){
+			*var = file;
+			return 1;
+		}
+		free(file);
+		break;
+	}
+	return 0;
+}
+
+int
+verbset(int obj, Plumbmsg *m, Rule *r, Exec *e)
+{
+	char *new;
+
+	switch(obj){
+	default:
+		fprint(2, "unimplemented 'is' object %d\n", obj);
+		break;
+	case OData:
+		new = estrdup(expand(e, r->arg, nil));
+		m->ndata = strlen(new);
+		free(m->data);
+		m->data = new;
+		e->p0 = -1;
+		e->p1 = -1;
+		e->setdata = 0;
+		return 1;
+	case ODst:
+		new = estrdup(expand(e, r->arg, nil));
+		free(m->dst);
+		m->dst = new;
+		return 1;
+	case OType:
+		new = estrdup(expand(e, r->arg, nil));
+		free(m->type);
+		m->type = new;
+		return 1;
+	case OWdir:
+		new = estrdup(expand(e, r->arg, nil));
+		free(m->wdir);
+		m->wdir = new;
+		return 1;
+	case OSrc:
+		new = estrdup(expand(e, r->arg, nil));
+		free(m->src);
+		m->src = new;
+		return 1;
+	}
+	return 0;
+}
+
+int
+verbadd(int obj, Plumbmsg *m, Rule *r, Exec *e)
+{
+	switch(obj){
+	default:
+		fprint(2, "unimplemented 'add' object %d\n", obj);
+		break;
+	case OAttr:
+		m->attr = plumbaddattr(m->attr, plumbunpackattr(expand(e, r->arg, nil)));
+		return 1;
+	}
+	return 0;
+}
+
+int
+verbdelete(int obj, Plumbmsg *m, Rule *r, Exec *e)
+{
+	char *a;
+
+	switch(obj){
+	default:
+		fprint(2, "unimplemented 'delete' object %d\n", obj);
+		break;
+	case OAttr:
+		a = expand(e, r->arg, nil);
+		if(plumblookup(m->attr, a) == nil)
+			break;
+		m->attr = plumbdelattr(m->attr, a);
+		return 1;
+	}
+	return 0;
+}
+
+int
+matchpat(Plumbmsg *m, Exec *e, Rule *r)
+{
+	switch(r->verb){
+	default:
+		fprint(2, "unimplemented verb %d\n", r->verb);
+		break;
+	case VAdd:
+		return verbadd(r->obj, m, r, e);
+	case VDelete:
+		return verbdelete(r->obj, m, r, e);
+	case VIs:
+		return verbis(r->obj, m, r);
+	case VIsdir:
+		return verbisfile(r->obj, m, r, e, DMDIR, 0, &e->dir);
+	case VIsfile:
+		return verbisfile(r->obj, m, r, e, ~DMDIR, DMDIR, &e->file);
+	case VMatches:
+		return verbmatches(r->obj, m, r, e);
+	case VSet:
+		verbset(r->obj, m, r, e);
+		return 1;
+	}
+	return 0;
+}
+
+void
+freeexec(Exec *exec)
+{
+	int i;
+
+	if(exec == nil)
+		return;
+	free(exec->dir);
+	free(exec->file);
+	for(i=0; i<10; i++)
+		free(exec->match[i]);
+	free(exec);
+}
+
+Exec*
+newexec(Plumbmsg *m)
+{
+	Exec *exec;
+	
+	exec = emalloc(sizeof(Exec));
+	exec->msg = m;
+	exec->p0 = -1;
+	exec->p1 = -1;
+	return exec;
+}
+
+void
+rewrite(Plumbmsg *m, Exec *e)
+{
+	Plumbattr *a, *prev;
+
+	if(e->clearclick){
+		prev = nil;
+		for(a=m->attr; a!=nil; a=a->next){
+			if(strcmp(a->name, "click") == 0){
+				if(prev == nil)
+					m->attr = a->next;
+				else
+					prev->next = a->next;
+				free(a->name);
+				free(a->value);	
+				free(a);
+				break;
+			}
+			prev = a;
+		}
+		if(e->setdata){
+			free(m->data);
+			m->data = estrdup(expand(e, "$0", nil));
+			m->ndata = strlen(m->data);
+		}
+	}
+}
+
+char**
+buildargv(char *s, Exec *e)
+{
+	char **av;
+	int ac;
+
+	ac = 0;
+	av = nil;
+	for(;;){
+		av = erealloc(av, (ac+1) * sizeof(char*));
+		av[ac] = nil;
+		while(*s==' ' || *s=='\t')
+			s++;
+		if(*s == '\0')
+			break;
+		av[ac++] = estrdup(expand(e, s, &s));
+	}
+	return av;
+}
+
+Exec*
+matchruleset(Plumbmsg *m, Ruleset *rs)
+{
+	int i;
+	Exec *exec;
+
+	if(m->dst!=nil && m->dst[0]!='\0' && rs->port!=nil && strcmp(m->dst, rs->port)!=0)
+		return nil;
+	exec = newexec(m);
+	for(i=0; i<rs->npat; i++)
+		if(!matchpat(m, exec, rs->pat[i])){
+			freeexec(exec);
+			return nil;
+		}
+	if(rs->port!=nil && (m->dst==nil || m->dst[0]=='\0')){
+		free(m->dst);
+		m->dst = estrdup(rs->port);
+	}
+	rewrite(m, exec);
+	return exec;
+}
+
+enum
+{
+	NARGS		= 100,
+	NARGCHAR	= 8*1024,
+	EXECSTACK 	= 4096+(NARGS+1)*sizeof(char*)+NARGCHAR
+};
+
+/* copy argv to stack and free the incoming strings, so we don't leak argument vectors */
+void
+stackargv(char **inargv, char *argv[NARGS+1], char args[NARGCHAR])
+{
+	int i, n;
+	char *s, *a;
+
+	s = args;
+	for(i=0; i<NARGS; i++){
+		a = inargv[i];
+		if(a == nil)
+			break;
+		n = strlen(a)+1;
+		if((s-args)+n >= NARGCHAR)	/* too many characters */
+			break;
+		argv[i] = s;
+		memmove(s, a, n);
+		s += n;
+		free(a);
+	}
+	argv[i] = nil;
+}
+
+
+void
+execproc(void *v)
+{
+	char **av;
+	char buf[1024], *args[NARGS+1], argc[NARGCHAR];
+
+	rfork(RFFDG);
+	close(0);
+	open("/dev/null", OREAD);
+	av = v;
+	stackargv(av, args, argc);
+	free(av);
+	procexec(nil, args[0], args);
+	if(args[0][0]!='/' && strncmp(args[0], "./", 2)!=0 && strncmp(args[0], "../", 3)!=0)
+		snprint(buf, sizeof buf, "/bin/%s", args[0]);
+	procexec(nil, buf, args);
+	threadexits("can't exec");
+}
+
+char*
+startup(Ruleset *rs, Exec *e)
+{
+	char **argv;
+	int i;
+
+	if(rs != nil)
+		for(i=0; i<rs->nact; i++){
+			if(rs->act[i]->verb == VStart)
+				goto Found;
+			if(rs->act[i]->verb == VClient){
+				if(e->msg->dst==nil || e->msg->dst[0]=='\0')
+					return "no port for \"client\" rule";
+				e->holdforclient = 1;
+				goto Found;
+			}
+		}
+	return "no start action for plumb message";
+
+Found:
+	argv = buildargv(rs->act[i]->arg, e);
+	if(argv[0] == nil)
+		return "empty argument list";
+	proccreate(execproc, argv, EXECSTACK);
+	return nil;
+}
blob - /dev/null
blob + d6a146541af7fba17ccb02ce4329d7bcd78ea783 (mode 644)
--- /dev/null
+++ src/cmd/plumb/mkfile
@@ -0,0 +1,20 @@
+</$objtype/mkfile
+
+TARG=plumber plumb
+
+
+BIN=/$objtype/bin
+</sys/src/cmd/mkmany
+
+PLUMBER=plumber.$O fsys.$O match.$O rules.$O
+PLUMB=plumb.$O
+
+$PLUMBER:	$HFILES plumber.h
+$PLUMB:		$HFILES
+
+$O.plumb:	$PLUMB
+$O.plumber:	$PLUMBER
+
+syms:V:
+	8c -a plumber.c	>syms
+	8c -aa fsys.c match.c rules.c >>syms
blob - /dev/null
blob + e0cff91279529ac1f68fa230001e1c1d6e763242 (mode 644)
--- /dev/null
+++ src/cmd/plumb/plumb.c
@@ -0,0 +1,119 @@
+#include <u.h>
+#include <libc.h>
+#include <plumb.h>
+
+char *plumbfile = nil;
+Plumbmsg m;
+
+void
+usage(void)
+{
+	fprint(2, "usage:  plumb [-p plumbfile] [-a 'attr=value ...'] [-s src] [-d dst] [-t type] [-w wdir] -i | data1\n");
+	exits("usage");
+}
+
+void
+gather(void)
+{
+	char buf[8192];
+	int n;
+
+	m.ndata = 0;
+	m.data = nil;
+	while((n = read(0, buf, sizeof buf)) > 0){
+		m.data = realloc(m.data, m.ndata+n);
+		if(m.data == nil){
+			fprint(2, "plumb: alloc failed: %r\n");
+			exits("alloc");
+		}
+		memmove(m.data+m.ndata, buf, n);
+		m.ndata += n;
+	}
+	if(n < 0){
+		fprint(2, "plumb: i/o error on input: %r\n");
+		exits("read");
+	}
+}
+
+void
+main(int argc, char *argv[])
+{
+	char buf[1024], *p;
+	int fd, i, input;
+
+	input = 0;
+	m.src = "plumb";
+	m.dst = nil;
+	m.wdir = getwd(buf, sizeof buf);
+	m.type = "text";
+	m.attr = nil;
+	ARGBEGIN{
+	case 'a':
+		p = ARGF();
+		if(p == nil)
+			usage();
+		m.attr = plumbaddattr(m.attr, plumbunpackattr(p));
+		break;
+	case 'd':
+		m.dst = ARGF();
+		if(m.dst == nil)
+			usage();
+		break;
+	case 'i':
+		input++;
+		break;
+	case 't':
+	case 'k':	/* for backwards compatibility */
+		m.type = ARGF();
+		if(m.type == nil)
+			usage();
+		break;
+	case 'p':
+		plumbfile = ARGF();
+		if(plumbfile == nil)
+			usage();
+		break;
+	case 's':
+		m.src = ARGF();
+		if(m.src == nil)
+			usage();
+		break;
+	case 'w':
+		m.wdir = ARGF();
+		if(m.wdir == nil)
+			usage();
+		break;
+	}ARGEND
+
+	if((input && argc>0) || (!input && argc<1))
+		usage();
+	if(plumbfile != nil)
+		fd = open(plumbfile, OWRITE);
+	else
+		fd = plumbopen("send", OWRITE);
+	if(fd < 0){
+		fprint(2, "plumb: can't open plumb file: %r\n");
+		exits("open");
+	}
+	if(input){
+		gather();
+		if(plumblookup(m.attr, "action") == nil)
+			m.attr = plumbaddattr(m.attr, plumbunpackattr("action=showdata"));
+		if(plumbsend(fd, &m) < 0){
+			fprint(2, "plumb: can't send message: %r\n");
+			exits("error");
+		}
+		exits(nil);
+	}
+	for(i=0; i<argc; i++){
+		if(input == 0){
+			m.data = argv[i];
+			m.ndata = -1;
+		}
+		if(plumbsend(fd, &m) < 0){
+			fprint(2, "plumb: can't send message: %r\n");
+			exits("error");
+		}
+	}
+	exits(nil);
+}
blob - /dev/null
blob + d0bd9c14e33ed9cadeeefc12c3ac6cd5573d1bc1 (mode 644)
--- /dev/null
+++ src/cmd/plumb/plumber.c
@@ -0,0 +1,147 @@
+#include <u.h>
+#include <libc.h>
+#include <regexp.h>
+#include <thread.h>
+#include <plumb.h>
+#include <auth.h>
+#include <fcall.h>
+#include "plumber.h"
+
+char	*plumbfile;
+char *user;
+char *home;
+char *progname;
+Ruleset **rules;
+int	printerrors=1;
+jmp_buf	parsejmp;
+char	*lasterror;
+
+void
+makeports(Ruleset *rules[])
+{
+	int i;
+
+	for(i=0; rules[i]; i++)
+		addport(rules[i]->port);
+}
+
+void
+mainproc(void *v)
+{
+	Channel *c;
+
+	c = v;
+	printerrors = 0;
+	makeports(rules);
+	startfsys();
+	sendp(c, nil);
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char buf[512];
+	int fd;
+	Channel *c;
+
+	progname = "plumber";
+
+	ARGBEGIN{
+	case 'p':
+		plumbfile = ARGF();
+		break;
+	}ARGEND
+
+	user = getenv("user");
+	home = getenv("home");
+	if(user==nil || home==nil)
+		error("can't initialize $user or $home: %r");
+	if(plumbfile == nil){
+		sprint(buf, "%s/lib/plumbing", home);
+		plumbfile = estrdup(buf);
+	}
+
+	fd = open(plumbfile, OREAD);
+	if(fd < 0)
+		error("can't open rules file %s: %r", plumbfile);
+	if(setjmp(parsejmp))
+		error("parse error");
+
+	rules = readrules(plumbfile, fd);
+	close(fd);
+
+	/*
+	 * Start all processes and threads from other proc
+	 * so we (main pid) can return to user.
+	 */
+	c = chancreate(sizeof(void*), 0);
+	proccreate(mainproc, c, 8192);
+	recvp(c);
+	chanfree(c);
+	threadexits(nil);
+}
+
+void
+error(char *fmt, ...)
+{
+	char buf[512];
+	va_list args;
+
+	va_start(args, fmt);
+	vseprint(buf, buf+sizeof buf, fmt, args);
+	va_end(args);
+
+	fprint(2, "%s: %s\n", progname, buf);
+	threadexitsall("error");
+}
+
+void
+parseerror(char *fmt, ...)
+{
+	char buf[512];
+	va_list args;
+
+	va_start(args, fmt);
+	vseprint(buf, buf+sizeof buf, fmt, args);
+	va_end(args);
+
+	if(printerrors){
+		printinputstack();
+		fprint(2, "%s\n", buf);
+	}
+	do; while(popinput());
+	lasterror = estrdup(buf);
+	longjmp(parsejmp, 1);
+}
+
+void*
+emalloc(long n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("malloc failed: %r");
+	memset(p, 0, n);
+	return p;
+}
+
+void*
+erealloc(void *p, long n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		error("realloc failed: %r");
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = strdup(s);
+	if(t == nil)
+		error("estrdup failed: %r");
+	return t;
+}
blob - /dev/null
blob + 0d1205f988acdf04ae3d7e1ed02b176948ad500d (mode 644)
--- /dev/null
+++ src/cmd/plumb/plumber.h
@@ -0,0 +1,93 @@
+typedef struct Exec Exec;
+typedef struct Rule Rule;
+typedef struct Ruleset Ruleset;
+
+/*
+ * Object
+ */
+enum
+{
+	OArg,
+	OAttr,
+	OData,
+	ODst,
+	OPlumb,
+	OSrc,
+	OType,
+	OWdir,
+};
+
+/*
+ * Verbs
+ */
+enum
+{
+	VAdd,	/* apply to OAttr only */
+	VClient,
+	VDelete,	/* apply to OAttr only */
+	VIs,
+	VIsdir,
+	VIsfile,
+	VMatches,
+	VSet,
+	VStart,
+	VTo,
+};
+
+struct Rule
+{
+	int	obj;
+	int	verb;
+	char	*arg;		/* unparsed string of all arguments */
+	char	*qarg;	/* quote-processed arg string */
+	Reprog	*regex;
+};
+
+struct Ruleset
+{
+	int	npat;
+	int	nact;
+	Rule	**pat;
+	Rule	**act;
+	char	*port;
+};
+
+struct Exec
+{
+	Plumbmsg	*msg;
+	char			*match[10];
+	int			p0;		/* begin and end of match */
+	int			p1;
+	int			clearclick;	/* click was expanded; remove attribute */
+	int			setdata;	/* data should be set to $0 */
+	int			holdforclient;	/* exec'ing client; keep message until port is opened */
+	/* values of $variables */
+	char			*file;
+	char 			*dir;
+};
+
+void		parseerror(char*, ...);
+void		error(char*, ...);
+void*	emalloc(long);
+void*	erealloc(void*, long);
+char*	estrdup(char*);
+Ruleset**	readrules(char*, int);
+void		startfsys(void);
+Exec*	matchruleset(Plumbmsg*, Ruleset*);
+void		freeexec(Exec*);
+char*	startup(Ruleset*, Exec*);
+char*	printrules(void);
+void		addport(char*);
+char*	writerules(char*, int);
+char*	expand(Exec*, char*, char**);
+void		makeports(Ruleset*[]);
+void		printinputstack(void);
+int		popinput(void);
+
+Ruleset	**rules;
+char		*user;
+char		*home;
+jmp_buf	parsejmp;
+char		*lasterror;
+char		**ports;
+int		nports;
blob - /dev/null
blob + 262f6d67f53928c16aeccc2964e46cc974dc3fbe (mode 644)
--- /dev/null
+++ src/cmd/plumb/rules.c
@@ -0,0 +1,779 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <regexp.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "plumber.h"
+
+typedef struct Input Input;
+typedef struct Var Var;
+
+struct Input
+{
+	char		*file;		/* name of file */
+	Biobuf	*fd;		/* input buffer, if from real file */
+	uchar	*s;		/* input string, if from /mnt/plumb/rules */
+	uchar	*end;	/* end of input string */
+	int		lineno;
+	Input	*next;	/* file to read after EOF on this one */
+};
+
+struct Var
+{
+	char	*name;
+	char	*value;
+	char *qvalue;
+};
+
+static int		parsing;
+static int		nvars;
+static Var		*vars;
+static Input	*input;
+
+static char 	ebuf[4096];
+
+char *badports[] =
+{
+	".",
+	"..",
+	"send",
+	nil
+};
+
+char *objects[] =
+{
+	"arg",
+	"attr",
+	"data",
+	"dst",
+	"plumb",
+	"src",
+	"type",
+	"wdir",
+	nil
+};
+
+char *verbs[] =
+{
+	"add",
+	"client",
+	"delete",
+	"is",
+	"isdir",
+	"isfile",
+	"matches",
+	"set",
+	"start",
+	"to",
+	nil
+};
+
+static void
+printinputstackrev(Input *in)
+{
+	if(in == nil)
+		return;
+	printinputstackrev(in->next);
+	fprint(2, "%s:%d: ", in->file, in->lineno);
+}
+
+void
+printinputstack(void)
+{
+	printinputstackrev(input);
+}
+
+static void
+pushinput(char *name, int fd, uchar *str)
+{
+	Input *in;
+	int depth;
+
+	depth = 0;
+	for(in=input; in; in=in->next)
+		if(depth++ >= 10)	/* prevent deep C stack in plumber and bad include structure */
+			parseerror("include stack too deep; max 10");
+
+	in = emalloc(sizeof(Input));
+	in->file = estrdup(name);
+	in->next = input;
+	input = in;
+	if(str)
+		in->s = str;
+	else{
+		in->fd = emalloc(sizeof(Biobuf));
+		if(Binit(in->fd, fd, OREAD) < 0)
+			parseerror("can't initialize Bio for rules file: %r");
+	}
+
+}
+
+int
+popinput(void)
+{
+	Input *in;
+
+	in = input;
+	if(in == nil)
+		return 0;
+	input = in->next;
+	if(in->fd){
+		Bterm(in->fd);
+		free(in->fd);
+	}
+	free(in);
+	return 1;
+}
+
+int
+getc(void)
+{
+	if(input == nil)
+		return Beof;
+	if(input->fd)
+		return Bgetc(input->fd);
+	if(input->s < input->end)
+		return *(input->s)++;
+	return -1;
+}
+
+char*
+getline(void)
+{
+	static int n = 0;
+	static char *s, *incl;
+	int c, i;
+
+	i = 0;
+	for(;;){
+		c = getc();
+		if(c < 0)
+			return nil;
+		if(i == n){
+			n += 100;
+			s = erealloc(s, n);
+		}
+		if(c<0 || c=='\0' || c=='\n')
+			break;
+		s[i++] = c;
+	}
+	s[i] = '\0';
+	return s;
+}
+
+int
+lookup(char *s, char *tab[])
+{
+	int i;
+
+	for(i=0; tab[i]!=nil; i++)
+		if(strcmp(s, tab[i])==0)
+			return i;
+	return -1;
+}
+
+Var*
+lookupvariable(char *s, int n)
+{
+	int i;
+
+	for(i=0; i<nvars; i++)
+		if(n==strlen(vars[i].name) && memcmp(s, vars[i].name, n)==0)
+			return vars+i;
+	return nil;
+}
+
+char*
+variable(char *s, int n)
+{
+	Var *var;
+
+	var = lookupvariable(s, n);
+	if(var)
+		return var->qvalue;
+	return nil;
+}
+
+void
+setvariable(char  *s, int n, char *val, char *qval)
+{
+	Var *var;
+
+	var = lookupvariable(s, n);
+	if(var){
+		free(var->value);
+		free(var->qvalue);
+	}else{
+		vars = erealloc(vars, (nvars+1)*sizeof(Var));
+		var = vars+nvars++;
+		var->name = emalloc(n+1);
+		memmove(var->name, s, n);
+	}
+	var->value = estrdup(val);
+	var->qvalue = estrdup(qval);
+}
+
+static char*
+nonnil(char *s)
+{
+	if(s == nil)
+		return "";
+	return s;
+}
+
+static char*
+filename(Exec *e, char *name)
+{
+	static char *buf;	/* rock to hold value so we don't leak the strings */
+
+	free(buf);
+	/* if name is defined, used it */
+	if(name!=nil && name[0]!='\0'){
+		buf = estrdup(name);
+		return cleanname(buf);
+	}
+	/* if data is an absolute file name, or wdir is empty, use it */
+	if(e->msg->data[0]=='/' || e->msg->wdir==nil || e->msg->wdir[0]=='\0'){
+		buf = estrdup(e->msg->data);
+		return cleanname(buf);
+	}
+	buf = emalloc(strlen(e->msg->wdir)+1+strlen(e->msg->data)+1);
+	sprint(buf, "%s/%s", e->msg->wdir, e->msg->data);
+	return cleanname(buf);
+}
+
+char*
+dollar(Exec *e, char *s, int *namelen)
+{
+	int n;
+	static char *abuf;
+	char *t;
+
+	*namelen = 1;
+	if(e!=nil && '0'<=s[0] && s[0]<='9')
+		return nonnil(e->match[s[0]-'0']);
+
+	for(t=s; isalnum(*t); t++)
+		;
+	n = t-s;
+	*namelen = n;
+
+	if(e != nil){
+		if(n == 3){
+			if(memcmp(s, "src", 3) == 0)
+				return nonnil(e->msg->src);
+			if(memcmp(s, "dst", 3) == 0)
+				return nonnil(e->msg->dst);
+			if(memcmp(s, "dir", 3) == 0)
+				return filename(e, e->dir);
+		}
+		if(n == 4){
+			if(memcmp(s, "attr", 4) == 0){
+				free(abuf);
+				abuf = plumbpackattr(e->msg->attr);
+				return nonnil(abuf);
+			}
+			if(memcmp(s, "data", 4) == 0)
+				return nonnil(e->msg->data);
+			if(memcmp(s, "file", 4) == 0)
+				return filename(e, e->file);
+			if(memcmp(s, "type", 4) == 0)
+				return nonnil(e->msg->type);
+			if(memcmp(s, "wdir", 3) == 0)
+				return nonnil(e->msg->wdir);
+		}
+	}
+
+	return variable(s, n);
+}
+
+/* expand one blank-terminated string, processing quotes and $ signs */
+char*
+expand(Exec *e, char *s, char **ends)
+{
+	char *p, *ep, *val;
+	int namelen, quoting;
+
+	p = ebuf;
+	ep = ebuf+sizeof ebuf-1;
+	quoting = 0;
+	while(p<ep && *s!='\0' && (quoting || (*s!=' ' && *s!='\t'))){
+		if(*s == '\''){
+			s++;
+			if(!quoting)
+				quoting = 1;
+			else  if(*s == '\''){
+				*p++ = '\'';
+				s++;
+			}else
+				quoting = 0;
+			continue;
+		}
+		if(quoting || *s!='$'){
+			*p++ = *s++;
+			continue;
+		}
+		s++;
+		val = dollar(e, s, &namelen);
+		if(val == nil){
+			*p++ = '$';
+			continue;
+		}
+		if(ep-p < strlen(val))
+			return "string-too-long";
+		strcpy(p, val);
+		p += strlen(val);
+		s += namelen;
+	}
+	if(ends)
+		*ends = s;
+	*p = '\0';
+	return ebuf;
+}
+
+void
+regerror(char *msg)
+{
+	if(parsing){
+		parsing = 0;
+		parseerror("%s", msg);
+	}
+	error("%s", msg);
+}
+
+void
+parserule(Rule *r)
+{
+	r->qarg = estrdup(expand(nil, r->arg, nil));
+	switch(r->obj){
+	case OArg:
+	case OAttr:
+	case OData:
+	case ODst:
+	case OType:
+	case OWdir:
+	case OSrc:
+		if(r->verb==VClient || r->verb==VStart || r->verb==VTo)
+			parseerror("%s not valid verb for object %s", verbs[r->verb], objects[r->obj]);
+		if(r->obj!=OAttr && (r->verb==VAdd || r->verb==VDelete))
+			parseerror("%s not valid verb for object %s", verbs[r->verb], objects[r->obj]);
+		if(r->verb == VMatches){
+			r->regex = regcomp(r->qarg);
+			return;
+		}
+		break;
+	case OPlumb:
+		if(r->verb!=VClient && r->verb!=VStart && r->verb!=VTo)
+			parseerror("%s not valid verb for object %s", verbs[r->verb], objects[r->obj]);
+		break;
+	}
+}
+
+int
+assignment(char *p)
+{
+	char *var, *qval;
+	int n;
+
+	if(!isalpha(p[0]))
+		return 0;
+	for(var=p; isalnum(*p); p++)
+		;
+	n = p-var;
+	while(*p==' ' || *p=='\t')
+			p++;
+	if(*p++ != '=')
+		return 0;
+	while(*p==' ' || *p=='\t')
+			p++;
+	qval = expand(nil, p, nil);
+	setvariable(var, n, p, qval);
+	return 1;
+}
+
+int
+include(char *s)
+{
+	char *t, *args[3], buf[128];
+	int n, fd;
+
+	if(strncmp(s, "include", 7) != 0)
+		return 0;
+	/* either an include or an error */
+	n = tokenize(s, args, nelem(args));
+	if(n < 2)
+		goto Err;
+	if(strcmp(args[0], "include") != 0)
+		goto Err;
+	if(args[1][0] == '#')
+		goto Err;
+	if(n>2 && args[2][0] != '#')
+		goto Err;
+	t = args[1];
+	fd = open(t, OREAD);
+	if(fd<0 && t[0]!='/' && strncmp(t, "./", 2)!=0 && strncmp(t, "../", 3)!=0){
+		snprint(buf, sizeof buf, "/sys/lib/plumb/%s", t);
+		t = buf;
+		fd = open(t, OREAD);
+	}
+	if(fd < 0)
+		parseerror("can't open %s for inclusion", t);
+	pushinput(t, fd, nil);
+	return 1;
+
+    Err:
+	parseerror("malformed include statement");
+	return 0;
+}
+
+Rule*
+readrule(int *eof)
+{
+	Rule *rp;
+	char *line, *p;
+	char *word;
+
+Top:
+	line = getline();
+	if(line == nil){
+		/*
+		 * if input is from string, and bytes remain (input->end is within string),
+		 * morerules() will pop input and save remaining data.  otherwise pop
+		 * the stack here, and if there's more input, keep reading.
+		 */
+		if((input!=nil && input->end==nil) && popinput())
+			goto Top;
+		*eof = 1;
+		return nil;
+	}
+	input->lineno++;
+
+	for(p=line; *p==' ' || *p=='\t'; p++)
+		;
+	if(*p=='\0' || *p=='#')	/* empty or comment line */
+		return nil;
+
+	if(include(p))
+		goto Top;
+
+	if(assignment(p))
+		return nil;
+
+	rp = emalloc(sizeof(Rule));
+
+	/* object */
+	for(word=p; *p!=' ' && *p!='\t'; p++)
+		if(*p == '\0')
+			parseerror("malformed rule");
+	*p++ = '\0';
+	rp->obj = lookup(word, objects);
+	if(rp->obj < 0){
+		if(strcmp(word, "kind") == 0)	/* backwards compatibility */
+			rp->obj = OType;
+		else
+			parseerror("unknown object %s", word);
+	}
+
+	/* verb */
+	while(*p==' ' || *p=='\t')
+		p++;
+	for(word=p; *p!=' ' && *p!='\t'; p++)
+		if(*p == '\0')
+			parseerror("malformed rule");
+	*p++ = '\0';
+	rp->verb = lookup(word, verbs);
+	if(rp->verb < 0)
+		parseerror("unknown verb %s", word);
+
+	/* argument */
+	while(*p==' ' || *p=='\t')
+		p++;
+	if(*p == '\0')
+		parseerror("malformed rule");
+	rp->arg = estrdup(p);
+
+	parserule(rp);
+
+	return rp;
+}
+
+void
+freerule(Rule *r)
+{
+	free(r->arg);
+	free(r->qarg);
+	free(r->regex);
+}
+
+void
+freerules(Rule **r)
+{
+	while(*r)
+		freerule(*r++);
+}
+
+void
+freeruleset(Ruleset *rs)
+{
+	freerules(rs->pat);
+	free(rs->pat);
+	freerules(rs->act);
+	free(rs->act);
+	free(rs->port);
+	free(rs);
+}
+
+Ruleset*
+readruleset(void)
+{
+	Ruleset *rs;
+	Rule *r;
+	int eof, inrule, i, ncmd;
+
+   Again:
+	eof = 0;
+	rs = emalloc(sizeof(Ruleset));
+	rs->pat = emalloc(sizeof(Rule*));
+	rs->act = emalloc(sizeof(Rule*));
+	inrule = 0;
+	ncmd = 0;
+	for(;;){
+		r = readrule(&eof);
+		if(eof)
+			break;
+		if(r==nil){
+			if(inrule)
+				break;
+			continue;
+		}
+		inrule = 1;
+		switch(r->obj){
+		case OArg:
+		case OAttr:
+		case OData:
+		case ODst:
+		case OType:
+		case OWdir:
+		case OSrc:
+			rs->npat++;
+			rs->pat = erealloc(rs->pat, (rs->npat+1)*sizeof(Rule*));
+			rs->pat[rs->npat-1] = r;
+			rs->pat[rs->npat] = nil;
+			break;
+		case OPlumb:
+			rs->nact++;
+			rs->act = erealloc(rs->act, (rs->nact+1)*sizeof(Rule*));
+			rs->act[rs->nact-1] = r;
+			rs->act[rs->nact] = nil;
+			if(r->verb == VTo){
+				if(rs->npat>0 && rs->port != nil)	/* npat==0 implies port declaration */
+					parseerror("too many ports");
+				if(lookup(r->qarg, badports) >= 0)
+					parseerror("illegal port name %s", r->qarg);
+				rs->port = estrdup(r->qarg);
+			}else
+				ncmd++;	/* start or client rule */
+			break;
+		}
+	}
+	if(ncmd > 1){
+		freeruleset(rs);
+		parseerror("ruleset has more than one client or start action");
+	}
+	if(rs->npat>0 && rs->nact>0)
+		return rs;
+	if(rs->npat==0 && rs->nact==0){
+		freeruleset(rs);
+		return nil;
+	}
+	if(rs->nact==0 || rs->port==nil){
+		freeruleset(rs);
+		parseerror("ruleset must have patterns and actions");
+		return nil;
+	}
+
+	/* declare ports */
+	for(i=0; i<rs->nact; i++)
+		if(rs->act[i]->verb != VTo){
+			freeruleset(rs);
+			parseerror("ruleset must have actions");
+			return nil;
+		}
+	for(i=0; i<rs->nact; i++)
+		addport(rs->act[i]->qarg);
+	freeruleset(rs);
+	goto Again;
+}
+
+Ruleset**
+readrules(char *name, int fd)
+{
+	Ruleset *rs, **rules;
+	int n;
+
+	parsing = 1;
+	pushinput(name, fd, nil);
+	rules = emalloc(sizeof(Ruleset*));
+	for(n=0; (rs=readruleset())!=nil; n++){
+		rules = erealloc(rules, (n+2)*sizeof(Ruleset*));
+		rules[n] = rs;
+		rules[n+1] = nil;
+	}
+	popinput();
+	parsing = 0;
+	return rules;
+}
+
+char*
+concat(char *s, char *t)
+{
+	if(t == nil)
+		return s;
+	if(s == nil)
+		s = estrdup(t);
+	else{
+		s = erealloc(s, strlen(s)+strlen(t)+1);
+		strcat(s, t);
+	}
+	return s;
+}
+
+char*
+printpat(Rule *r)
+{
+	char *s;
+
+	s = emalloc(strlen(objects[r->obj])+1+strlen(verbs[r->verb])+1+strlen(r->arg)+1+1);
+	sprint(s, "%s\t%s\t%s\n", objects[r->obj], verbs[r->verb], r->arg);
+	return s;
+}
+
+char*
+printvar(Var *v)
+{
+	char *s;
+
+	s = emalloc(strlen(v->name)+1+strlen(v->value)+2+1);
+	sprint(s, "%s=%s\n\n", v->name, v->value);
+	return s;
+}
+
+char*
+printrule(Ruleset *r)
+{
+	int i;
+	char *s;
+
+	s = nil;
+	for(i=0; i<r->npat; i++)
+		s = concat(s, printpat(r->pat[i]));
+	for(i=0; i<r->nact; i++)
+		s = concat(s, printpat(r->act[i]));
+	s = concat(s, "\n");
+	return s;
+}
+
+char*
+printport(char *port)
+{
+	char *s;
+
+	s = nil;
+	s = concat(s, "plumb to ");
+	s = concat(s, port);
+	s = concat(s, "\n");
+	return s;
+}
+
+char*
+printrules(void)
+{
+	int i;
+	char *s;
+
+	s = nil;
+	for(i=0; i<nvars; i++)
+		s = concat(s, printvar(&vars[i]));
+	for(i=0; i<nports; i++)
+		s = concat(s, printport(ports[i]));
+	s = concat(s, "\n");
+	for(i=0; rules[i]; i++)
+		s = concat(s, printrule(rules[i]));
+	return s;
+}
+
+char*
+stringof(char *s, int n)
+{
+	char *t;
+
+	t = emalloc(n+1);
+	memmove(t, s, n);
+	return t;
+}
+
+uchar*
+morerules(uchar *text, int done)
+{
+	int n;
+	Ruleset *rs;
+	uchar *otext, *s, *endofrule;
+
+	pushinput("<rules input>", -1, text);
+	if(done)
+		input->end = text+strlen((char*)text);
+	else{
+		/*
+		 * Help user by sending any full rules to parser so any parse errors will
+		 * occur on write rather than close. A heuristic will do: blank line ends rule.
+		 */
+		endofrule = nil;
+		for(s=text; *s!='\0'; s++)
+			if(*s=='\n' && *++s=='\n')
+				endofrule = s+1;
+		if(endofrule == nil)
+			return text;
+		input->end = endofrule;
+	}
+	for(n=0; rules[n]; n++)
+		;
+	while((rs=readruleset()) != nil){
+		rules = erealloc(rules, (n+2)*sizeof(Ruleset*));
+		rules[n++] = rs;
+		rules[n] = nil;
+	}
+	otext =text;
+	if(input == nil)
+		text = (uchar*)estrdup("");
+	else
+		text = (uchar*)estrdup((char*)input->end);
+	popinput();
+	free(otext);
+	return text;
+}
+
+char*
+writerules(char *s, int n)
+{
+	static uchar *text;
+	char *tmp;
+
+	free(lasterror);
+	lasterror = nil;
+	parsing = 1;
+	if(setjmp(parsejmp) == 0){
+		tmp = stringof(s, n);
+		text = (uchar*)concat((char*)text, tmp);
+		free(tmp);
+		text = morerules(text, s==nil);
+	}
+	if(s == nil){
+		free(text);
+		text = nil;
+	}
+	parsing = 0;
+	makeports(rules);
+	return lasterror;
+}