Commit Diff


commit - 7a89af2b85e5db320ff2be364cb6c0cc0473663d
commit + 423f02f5dbaa7b68b446483dc25e22b00db8a07e
blob - 024927cfcf8dc33f61b3656e18d9a92471c0f326
blob + ad3f4c31452cba5bba00a25368429af6966e770a
--- TODO
+++ TODO
@@ -8,3 +8,9 @@
    user and not by root.
 
  - tweak iounit so it's always lower than the choosen msize
+
+ - handle message bigger than MAX_IMSGSIZE - IMSG_HEADER_SIZE.  One
+   way to do that would be to *not* use asynchroonus imsgs in client.c
+   but synchronous I/O: this way, once a message has been processed,
+   we can just receive the next in the same function (i.e. twrite) and
+   go ahead.
blob - d3484c718bade31e04b234150c8dd206f4ac75d2
blob + ea9b319fd27f64cd69cd2b5366f4fae9b549471e
--- client.c
+++ client.c
@@ -35,6 +35,15 @@
 #include "sandbox.h"
 #include "utils.h"
 
+/*
+ * XXX: atm is difficult to accept messages bigger than MAX_IMSGSIZE
+ * minus IMSG_HEADER_SIZE, we need something to split messages into
+ * chunks and receive them one by the other.
+ *
+ * CLIENT_MSIZE is thus the maximum message size we can handle now.
+ */
+#define CLIENT_MSIZE (MAX_IMSGSIZE - IMSG_HEADER_SIZE)
+
 #define DEBUG_PACKETS 0
 
 /* straight outta /src/usr.bin/ssh/scp.c */
@@ -42,12 +51,6 @@
 	((sizeof(type) == 4 && (val) > INT32_MAX) || \
 	 (sizeof(type) == 8 && (val) > INT64_MAX) || \
 	 (sizeof(type) != 4 && sizeof(type) != 8))
-
-struct qid {
-	uint64_t		 path;
-	uint32_t		 vers;
-	uint8_t			 type;
-};
 
 STAILQ_HEAD(dirhead, dir) dirs;
 struct dir {
@@ -861,7 +864,7 @@ tversion(struct np_msg_header *hdr, const uint8_t *dat
 
 	/* version matched */
 	handshaked = 1;
-	msize = MIN(msize, MSIZE9P);
+	msize = MIN(msize, CLIENT_MSIZE);
 	client_send_listener(IMSG_MSIZE, &msize, sizeof(msize));
 	np_version(hdr->tag, msize, VERSION9P);
 	return;
@@ -1387,7 +1390,7 @@ tread(struct np_msg_header *hdr, const uint8_t *data, 
 	}
 
 	if (TYPE_OVERFLOW(off_t, off)) {
-		log_warnx("unexpected size_t size");
+		log_warnx("unexpected off_t size");
 		np_error(hdr->tag, "invalid offset");
 		return;
 	}
@@ -1403,6 +1406,7 @@ tread(struct np_msg_header *hdr, const uint8_t *data, 
 		if (off == 0 && f->offset != 0) {
 			rewinddir(f->d);
 			f->offset = 0;
+			evbuffer_drain(f->evb, EVBUFFER_LENGTH(f->evb));
 		}
 
 		if (off != f->offset) {
blob - 6b3f71d033c44d0f05037b8cebd54c1c3e79580e
blob + c218227956175b3d089238ca1934f43f54e80105
--- ftp.c
+++ ftp.c
@@ -19,6 +19,7 @@
 #include <sys/types.h>
 #include <sys/socket.h>
 
+#include <assert.h>
 #include <netdb.h>
 #include <limits.h>
 #include <stdio.h>
@@ -28,11 +29,15 @@
 #include <tls.h>
 #include <unistd.h>
 
-#if HAVE_READLINE
+#if HAVE_LIBREADLINE
 #include <readline/readline.h>
 #include <readline/history.h>
 #endif
 
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
 #include "9pclib.h"
 #include "kamid.h"
 #include "utils.h"
@@ -47,10 +52,16 @@ const char	*keypath;
 struct tls_config	*tlsconf;
 struct tls		*ctx;
 int			 sock;
+struct evbuffer		*buf;
+struct evbuffer		*dirbuf;
+uint32_t		 msize;
+int			 bell;
 
 #define PWDFID		0
 
-#if HAVE_READLINE
+#define ASSERT_EMPTYBUF() assert(EVBUFFER_LENGTH(buf) == 0)
+
+#if HAVE_LIBREADLINE
 static char *
 read_line(const char *prompt)
 {
@@ -76,6 +87,9 @@ read_line(const char *prompt)
 	size_t linesize = 0;
 	ssize_t linelen;
 
+	printf("%s", prompt);
+	fflush(stdout);
+
 	linelen = getline(&line, &linesize, stdin);
 	if (linelen == -1)
 		return NULL;
@@ -96,22 +110,256 @@ usage(int ret)
 }
 
 static void
+do_send(void)
+{
+	ssize_t r;
+
+	while (EVBUFFER_LENGTH(evb) != 0) {
+		r = tls_write(ctx, EVBUFFER_DATA(evb), EVBUFFER_LENGTH(evb));
+		switch (r) {
+		case TLS_WANT_POLLIN:
+		case TLS_WANT_POLLOUT:
+			continue;
+		case -1:
+			errx(1, "tls: %s", tls_error(ctx));
+		default:
+			evbuffer_drain(evb, r);
+		}
+	}
+}
+
+static void
+mustread(void *d, size_t len)
+{
+	ssize_t r;
+
+	while (len != 0) {
+		switch (r = tls_read(ctx, d, len)) {
+		case TLS_WANT_POLLIN:
+		case TLS_WANT_POLLOUT:
+			continue;
+		case -1:
+			errx(1, "tls: %s", tls_error(ctx));
+		default:
+			d += r;
+			len -= r;
+		}
+	}
+}
+
+static void
+recv_msg(void)
+{
+	uint32_t	len;
+	ssize_t		r;
+	char		tmp[BUFSIZ];
+
+	mustread(&len, sizeof(len));
+	len = le32toh(len);
+	if (len < HEADERSIZE)
+		errx(1, "read message of invalid length %d", len);
+
+	len -= 4; /* skip the length just read */
+
+	while (len != 0) {
+		switch (r = tls_read(ctx, tmp, sizeof(tmp))) {
+		case TLS_WANT_POLLIN:
+		case TLS_WANT_POLLOUT:
+			continue;
+		case -1:
+			errx(1, "tls: %s", tls_error(ctx));
+		default:
+			len -= r;
+			evbuffer_add(buf, tmp, r);
+		}
+	}
+}
+
+static uint64_t
+np_read64(struct evbuffer *buf)
+{
+	uint64_t n;
+
+	evbuffer_remove(buf, &n, sizeof(n));
+	return le64toh(n);
+}
+
+static uint32_t
+np_read32(struct evbuffer *buf)
+{
+	uint32_t n;
+
+	evbuffer_remove(buf, &n, sizeof(n));
+	return le32toh(n);
+}
+
+static uint16_t
+np_read16(struct evbuffer *buf)
+{
+	uint16_t n;
+
+	evbuffer_remove(buf, &n, sizeof(n));
+	return le16toh(n);
+}
+
+static uint16_t
+np_read8(struct evbuffer *buf)
+{
+	uint8_t n;
+
+	evbuffer_remove(buf, &n, sizeof(n));
+	return n;
+}
+
+static char *
+np_readstr(struct evbuffer *buf)
+{
+	uint16_t	 len;
+	char		*str;
+
+	len = np_read16(buf);
+	assert(EVBUFFER_LENGTH(buf) >= len);
+
+	if ((str = calloc(1, len+1)) == NULL)
+		err(1, "calloc");
+	evbuffer_remove(buf, str, len);
+	return str;
+}
+
+static void
+np_read_qid(struct evbuffer *buf, struct qid *qid)
+{
+	assert(EVBUFFER_LENGTH(buf) >= QIDSIZE);
+
+	qid->type = np_read8(buf);
+	qid->vers = np_read32(buf);
+	qid->path = np_read64(buf);
+}
+
+static void
+expect(uint8_t type)
+{
+	uint8_t t;
+
+	t = np_read8(buf);
+	if (t == type)
+		return;
+
+	if (t == Terror) {
+		char *err;
+
+		err = np_readstr(buf);
+		errx(1, "expected %s, got error %s",
+		    pp_msg_type(type), err);
+	}
+
+	errx(1, "expected %s, got msg type %s",
+	    pp_msg_type(type), pp_msg_type(t));
+}
+
+static void
+expect2(uint8_t type, uint16_t tag)
+{
+	uint16_t t;
+
+	expect(type);
+
+	t = np_read16(buf);
+	if (t == tag)
+		return;
+
+	errx(1, "expected tag 0x%x, got 0x%x", tag, t);
+}
+
+static void
 do_version(void)
 {
+	char		*version;
+
 	tversion(VERSION9P, MSIZE9P);
-	/* TODO: get reply */
+	do_send();
+	recv_msg();
+	expect2(Rversion, NOTAG);
+
+	msize = np_read32(buf);
+	version = np_readstr(buf);
+
+	if (msize > MSIZE9P)
+		errx(1, "got unexpected msize: %d", msize);
+	if (strcmp(version, VERSION9P))
+		errx(1, "unexpected 9p version: %s", version);
+
+	free(version);
+	ASSERT_EMPTYBUF();
 }
 
 static void
 do_attach(const char *path)
 {
+	const char *user;
+	struct qid qid;
+
 	if (path == NULL)
 		path = "/";
+	if ((user = getenv("USER")) == NULL)
+		user = "flan";
 
-	/* TODO: do attach */
+	tattach(PWDFID, NOFID, user, path);
+	do_send();
+	recv_msg();
+	expect2(Rattach, iota_tag);
+	np_read_qid(buf, &qid);
+
+	ASSERT_EMPTYBUF();
+}
+
+static uint32_t
+do_open(uint32_t fid, uint8_t mode)
+{
+	struct qid qid;
+	uint32_t iounit;
+
+	topen(fid, mode);
+	do_send();
+	recv_msg();
+	expect2(Ropen, iota_tag);
+
+	np_read_qid(buf, &qid);
+	iounit = np_read32(buf);
+
+	ASSERT_EMPTYBUF();
+
+	return iounit;
 }
 
 static void
+do_clunk(uint32_t fid)
+{
+	tclunk(fid);
+	do_send();
+	recv_msg();
+	expect2(Rclunk, iota_tag);
+
+	ASSERT_EMPTYBUF();
+}
+
+static void
+dup_fid(int fid, int nfid)
+{
+	uint16_t nwqid;
+
+	twalk(fid, nfid, NULL, 0);
+	do_send();
+	recv_msg();
+	expect2(Rwalk, iota_tag);
+
+	nwqid = np_read16(buf);
+	assert(nwqid == 0);
+
+	ASSERT_EMPTYBUF();
+}
+
+static void
 do_connect(const char *connspec, const char *path)
 {
 	int handshake;
@@ -165,13 +413,175 @@ do_connect(const char *connspec, const char *path)
 	free(host);
 }
 
+static void
+cmd_bell(int argc, const char **argv)
+{
+	if (argc == 0) {
+		bell = !bell;
+		if (bell)
+			puts("bell mode enabled");
+		else
+			puts("bell mode disabled");
+		return;
+	}
+
+	if (argc != 1)
+		goto usage;
+
+	if (!strcmp(*argv, "on")) {
+		bell = 1;
+		puts("bell mode enabled");
+		return;
+	}
+
+	if (!strcmp(*argv, "off")) {
+		bell = 0;
+		puts("bell mode disabled");
+		return;
+	}
+
+usage:
+	printf("bell [on | off]\n");
+}
+
+static void
+cmd_bye(int argc, const char **argv)
+{
+	log_warnx("bye\n");
+	exit(0);
+}
+
+static void
+cmd_ls(int argc, const char **argv)
+{
+	uint64_t off = 0;
+	uint32_t len;
+
+	if (argc != 0) {
+		printf("ls don't take arguments (yet)\n");
+		return;
+	}
+
+	dup_fid(PWDFID, 1);
+	do_open(1, KOREAD);
+
+	evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf));
+
+	for (;;) {
+		tread(1, off, BUFSIZ);
+		do_send();
+		recv_msg();
+		expect2(Rread, iota_tag);
+
+		len = np_read32(buf);
+		if (len == 0)
+			break;
+
+		evbuffer_add_buffer(dirbuf, buf);
+		off += len;
+
+		ASSERT_EMPTYBUF();
+	}
+
+	while (EVBUFFER_LENGTH(dirbuf) != 0) {
+		struct qid	 qid;
+		uint64_t	 len;
+		uint16_t	 size;
+		char		*name;
+
+		size = np_read16(dirbuf);
+		assert(size <= EVBUFFER_LENGTH(dirbuf));
+
+		np_read16(dirbuf); /* skip type */
+		np_read32(dirbuf); /* skip dev */
+
+		np_read_qid(dirbuf, &qid);
+		printf("%s ", pp_qid_type(qid.type));
+
+		np_read32(dirbuf); /* skip mode */
+		np_read32(dirbuf); /* skip atime */
+		np_read32(dirbuf); /* skip mtime */
+
+		len = np_read64(dirbuf);
+		printf("%llu ", (unsigned long long)len);
+
+		name = np_readstr(dirbuf);
+		printf("%s\n", name);
+		free(name);
+
+		free(np_readstr(dirbuf)); /* skip uid */
+		free(np_readstr(dirbuf)); /* skip gid */
+		free(np_readstr(dirbuf)); /* skip muid */
+	}
+
+	do_clunk(1);
+}
+
+static void
+cmd_verbose(int argc, const char **argv)
+{
+	if (argc == 0) {
+		log_setverbose(!log_getverbose());
+		if (log_getverbose())
+			puts("verbose mode enabled");
+		else
+			puts("verbose mode disabled");
+		return;
+	}
+
+	if (argc != 1)
+		goto usage;
+
+	if (!strcmp(*argv, "on")) {
+		log_setverbose(1);
+		puts("verbose mode enabled");
+		return;
+	}
+
+	if (!strcmp(*argv, "off")) {
+		log_setverbose(0);
+		puts("verbose mode disabled");
+		return;
+	}
+
+usage:
+	printf("verbose [on | off]\n");
+}
+
+static void
+excmd(int argc, const char **argv)
+{
+	struct cmd {
+		const char	*name;
+		void		(*fn)(int, const char **);
+	} cmds[] = {
+		{"bell",	cmd_bell},
+		{"bye",		cmd_bye},
+		{"ls",		cmd_ls},
+		{"quit",	cmd_bye},
+		{"verbose",	cmd_verbose},
+	};
+	size_t i;
+
+	if (argc == 0)
+		return;
+	for (i = 0; i < nitems(cmds); ++i) {
+		if (!strcmp(cmds[i].name, *argv)) {
+			cmds[i].fn(argc-1, argv+1);
+			return;
+		}
+	}
+
+	log_warnx("unknown command %s", *argv);
+}
+
 int
 main(int argc, char **argv)
 {
 	int	ch;
 
 	log_init(1, LOG_DAEMON);
-	log_setverbose(1);
+	log_setverbose(0);
 	log_procinit(getprogname());
 
 	while ((ch = getopt(argc, argv, "C:cK:")) != -1) {
@@ -198,14 +608,36 @@ main(int argc, char **argv)
 	if ((evb = evbuffer_new()) == NULL)
 		fatal("evbuffer_new");
 
+	if ((buf = evbuffer_new()) == NULL)
+		fatal("evbuffer_new");
+
+	if ((dirbuf = evbuffer_new()) == NULL)
+		fatal("evbuferr_new");
+
 	do_connect(argv[0], argv[1]);
 
+	/* cmd_ls(0, NULL); */
+
 	for (;;) {
-		char *line;
+		int argc = 0;
+		char *line, *argv[16] = {0}, **ap;
 
-		if ((line = read_line("ftp> ")) == NULL)
+		if ((line = read_line("kamiftp> ")) == NULL)
 			break;
-		printf("read: %s\n", line);
+
+		for (argc = 0, ap = argv; ap < &argv[15] &&
+		    (*ap = strsep(&line, " \t")) != NULL;) {
+			if (**ap != '\0')
+				ap++, argc++;
+		}
+		excmd(argc, (const char **)argv);
+
+		if (bell) {
+			printf("\a");
+			fflush(stdout);
+		}
+
+		free(line);
 	}
 
 	printf("\n");
blob - 9fa99a3c7244b99f835976cae7bb1dc48e7cdfda
blob + 9675e6ac175f820b3eec23853740698a562b017b
--- kamid.h
+++ kamid.h
@@ -151,6 +151,12 @@ struct np_msg_header {
 	uint16_t	tag;
 };
 
+struct qid {
+	uint64_t		 path;
+	uint32_t		 vers;
+	uint8_t			 type;
+};
+
 /* useful constants */
 #define HEADERSIZE	(4 + 1 + 2)
 #define	VERSION9P	"9P2000"
blob - 1b88cb698ca30ff9bb277d2af0697b6fa3075734
blob + bbb82ddbd18431c0c4cc0cc05d9e9273e5bf4d26
--- kamiftp.1
+++ kamiftp.1
@@ -49,8 +49,29 @@ Specify the path to the client certificate
 .Ar key
 to be used during the TLS handshake.
 .El
+.Pp
+The following commands are recognized by
+.Nm :
+.Bl -tag -width Ds
+.It Ic bell Oo Cm on | off Oc
+Request terminal to sound a bell after each command.
+Without arguments toggle the current state.
+.It Ic bye
+Terminate the session.
+Synomym of
+.Ic quit .
+.It Ic ls
+List the file in the current directory
+.It Ic quit
+Terminate the session.
+Synomym of
+.Ic bye .
+.It Ic verbose Oo Cm on | off Oc
+Print verbose information.
+Without arguments toggle the current state.
+.El
 .Sh SEE ALSO
-.Xr 9p 7
+.Xr 9p 7 ,
 .Xr kamid 8
 .Sh AUTHORS
 The
blob - 6c7171faab707741aba6595e903666e9a77964eb
blob + 504329156629b6374f7a18342d97b66bfbb16659
--- kamirepl.c
+++ kamirepl.c
@@ -84,7 +84,6 @@ static void		 excmd_read(const char ** , int);
 static void		 excmd_write(const char **, int);
 static void		 excmd(const char **, int);
 
-static const char	*pp_qid_type(uint8_t);
 static void		 pp_qid(const uint8_t *, uint32_t);
 static void		 pp_msg(uint32_t, uint8_t, uint16_t, const uint8_t *);
 static void		 handle_9p(const uint8_t *, size_t);
@@ -702,23 +701,6 @@ excmd(const char **argv, int argc)
 	}
 
 	log_warnx("Unknown command %s", *argv);
-}
-
-static const char *
-pp_qid_type(uint8_t type)
-{
-        switch (type) {
-	case QTDIR:	return "dir";
-	case QTAPPEND:	return "append-only";
-	case QTEXCL:	return "exclusive";
-	case QTMOUNT:	return "mounted-channel";
-	case QTAUTH:	return "authentication";
-	case QTTMP:	return "non-backed-up";
-	case QTSYMLINK: return "symlink";
-	case QTFILE:	return "file";
-	}
-
-	return "unknown";
 }
 
 static void
blob - 1e9b8c93e6bd2a5ee92763875fd80ccd05987ef8
blob + 6da049b0758a48894158408f942e91823baa3ade
--- ninepscript.5
+++ ninepscript.5
@@ -69,10 +69,17 @@ It evaluates to the value of the variable or constant 
 scope.
 .It comparison
 The syntax is
-.Ql Ar expression Cm == Ar expression
+.Bd -literal -offset Ds
+.Ar expression Cm == Ar expression
+.Ar expression Cm <= Ar expression
+.Ed
+.Pp
 and yields a true value if the two expressions are considered to be
+respectively
 .Sq equal
-or a false value otherwise.
+or
+.Sq lesser equal ,
+a false value otherwise.
 Two values are equal if they are both number and represent the same
 value
 .Pq regardless of the size
@@ -168,9 +175,9 @@ Execute
 .Ar procedure
 with the given
 .Ar arguments .
-.It Ic assert Ar expression
+.It Ic assert Ar comparison
 Evaluate
-.Ar expression
+.Ar comparison
 and if it not yields a true-ish value terminate the current running
 test and mark it as failed.
 Multiple assertion can be done in one single
@@ -178,15 +185,15 @@ Multiple assertion can be done in one single
 block using the following syntax:
 .Bd -literal -offset Ds
 .Ic assert (
-	expression_1
-	expression_2
+	comparison_1
+	comparison_2
 	...
-	expression_n
+	comparison_n
 )
 .Ed
 .Pp
 Note that newlines are mandatory after every
-.Ar expression
+.Ar comparison
 in this case.
 .It Ic should-fail Ar expression Op : Ar reason
 Evaluate
blob - e856da804353a10ea38daeea0e7396e071ecbc58
blob + 3b22364359c6f834886ffda7912b580339df0524
--- np.y
+++ np.y
@@ -163,7 +163,9 @@ literal	: STRING		{ $$ = op_lit_str($1); }
  * interested in checking all the possibilities here.
  */
 cexpr	: literal | varref | funcall | faccess ;
-check	: cexpr '=' '=' cexpr	{ $$ = op_cmp_eq($1, $4); }	;
+check	: cexpr '=' '=' cexpr	{ $$ = op_cmp_eq($1, $4); }
+	| cexpr '<' '=' cexpr	{ $$ = op_cmp_leq($1, $4); }
+	;
 
 expr	: literal | funcall | varref | check | cast | faccess | vargs ;
 
blob - 68fd63cc1792b8a8158fe5a817c2794afa5ff875
blob + c5b25c870e154f06143d64be5bab83c66694c253
--- regress/lib.9ps
+++ regress/lib.9ps
@@ -33,7 +33,7 @@ proc mount(fid, path) {
 	assert (
 		m.type == Rversion
 		m.tag == notag
-		m.msize == msize
+		m.msize <= msize
 		# m.version == version
 	)
 
blob - 08f401c4930cd4e4cf94ab0ca4ece3cff6e04a93
blob + 8c80fde28a266bf7c9947cfd555b284fd9d5f801
--- regress/misc-suite.9ps
+++ regress/misc-suite.9ps
@@ -18,7 +18,7 @@ testing "multiple attach" dir "./root" {
 	assert (
 		m.type == Rversion
 		m.tag == notag
-		m.msize == msize
+		m.msize <= msize
 	)
 
 	fid1 = 0
blob - 4fb5bc0910430077c25c774212922762678ad98f
blob + 8b480c2ff8bd568c4adadc410eed846e8f7696d2
--- script.c
+++ script.c
@@ -428,8 +428,9 @@ free_op(struct op *op)
 		free_op_rec(op->v.cast.expr);
 		break;
 	case OP_CMP_EQ:
-		free_op_rec(op->v.cmp_eq.a);
-		free_op_rec(op->v.cmp_eq.b);
+	case OP_CMP_LEQ:
+		free_op_rec(op->v.bin_cmp.a);
+		free_op_rec(op->v.bin_cmp.b);
 		break;
 	case OP_FACCESS:
 		free_op_rec(op->v.faccess.expr);
@@ -517,9 +518,21 @@ op_cmp_eq(struct op *a, struct op *b)
 	struct op *op;
 
 	op = newop(OP_CMP_EQ);
-	op->v.cmp_eq.a = a;
-	op->v.cmp_eq.b = b;
+	op->v.bin_cmp.a = a;
+	op->v.bin_cmp.b = b;
+
+	return op;
+}
+
+struct op *
+op_cmp_leq(struct op *a, struct op *b)
+{
+	struct op *op;
 
+	op = newop(OP_CMP_LEQ);
+	op->v.bin_cmp.a = a;
+	op->v.bin_cmp.b = b;
+
 	return op;
 }
 
@@ -677,7 +690,15 @@ val_eq(struct value *a, struct value *b)
 	case V_SYM:
 		return !strcmp(a->v.str, b->v.str);
 	}
+
+	return 0;
+}
 
+int
+val_leq(struct value *a, struct value *b)
+{
+	if (val_isnum(a) && val_isnum(b))
+		return val_tonum(a) <= val_tonum(b);
 	return 0;
 }
 
@@ -879,10 +900,15 @@ pp_op(struct op *op)
 		}
 		break;
 	case OP_CMP_EQ:
-		pp_op(op->v.cmp_eq.a);
+		pp_op(op->v.bin_cmp.a);
 		printf(" == ");
-		pp_op(op->v.cmp_eq.b);
+		pp_op(op->v.bin_cmp.b);
 		break;
+	case OP_CMP_LEQ:
+		pp_op(op->v.bin_cmp.a);
+		printf(" <= ");
+		pp_op(op->v.bin_cmp.b);
+		break;
 	case OP_FACCESS:
 		pp_op(op->v.faccess.expr);
 		printf(".%s", op->v.faccess.field);
@@ -1052,15 +1078,25 @@ eval(struct op *op)
 		break;
 
 	case OP_CMP_EQ:
-		if ((ret = eval(op->v.cmp_eq.a)) != EVAL_OK)
+		if ((ret = eval(op->v.bin_cmp.a)) != EVAL_OK)
 			return ret;
-		if ((ret = eval(op->v.cmp_eq.b)) != EVAL_OK)
+		if ((ret = eval(op->v.bin_cmp.b)) != EVAL_OK)
 			return ret;
 
 		popv(&b);
 		popv(&a);
 		pushbool(val_eq(&a, &b));
+		break;
+
+	case OP_CMP_LEQ:
+		if ((ret = eval(op->v.bin_cmp.a)) != EVAL_OK)
+			return ret;
+		if ((ret = eval(op->v.bin_cmp.b)) != EVAL_OK)
+			return ret;
 
+		popv(&b);
+		popv(&a);
+		pushbool(val_leq(&a, &b));
 		break;
 
 	case OP_FACCESS:
blob - 8e094a0a1f2b051994a39fe747d5803f68d7bb85
blob + f7b6a07ae2b5f8ecc35bea64130cf0b6af2af2c0
--- script.h
+++ script.h
@@ -69,6 +69,7 @@ enum {
 	OP_VAR,
 	OP_CAST,
 	OP_CMP_EQ,
+	OP_CMP_LEQ,
 	OP_FACCESS,
 	OP_SFAIL,
 	OP_VARGS,
@@ -99,7 +100,7 @@ struct op {
 		struct {
 			struct op	*a;
 			struct op	*b;
-		} cmp_eq;
+		} bin_cmp;
 		struct {
 			struct op	*expr;
 			char		*field;
@@ -177,6 +178,7 @@ struct op	*op_var(char *);
 struct op	*op_lit_str(char *);
 struct op	*op_lit_num(uint64_t);
 struct op	*op_cmp_eq(struct op *, struct op *);
+struct op	*op_cmp_leq(struct op *, struct op *);
 struct op	*op_cast(struct op *, int);
 struct op	*op_faccess(struct op *, char *);
 struct op	*op_sfail(struct op *, char *);
@@ -190,6 +192,7 @@ int		 val_trueish(struct value *);
 int		 val_isnum(struct value *);
 int64_t		 val_tonum(struct value *);
 int		 val_eq(struct value *, struct value *);
+int		 val_leq(struct value *, struct value *);
 int		 val_cast(struct value *, int);
 int		 val_faccess(struct value *, const char *, struct value *);
 void		 pp_op(struct op *);
blob - 066fdb3d2905942ccc7328d712194ff6a03fc1a8
blob + 50b83e74a8d469e6fa1c0906a70fe5fd7fd61ca9
--- utils.c
+++ utils.c
@@ -102,6 +102,23 @@ pp_msg_type(uint8_t type)
 	}
 }
 
+const char *
+pp_qid_type(uint8_t type)
+{
+	switch (type) {
+	case QTDIR:     return "dir";
+	case QTAPPEND:  return "append-only";
+	case QTEXCL:    return "exclusive";
+	case QTMOUNT:   return "mounted-channel";
+	case QTAUTH:    return "authentication";
+	case QTTMP:     return "non-backed-up";
+	case QTSYMLINK: return "symlink";
+	case QTFILE:    return "file";
+	}
+	
+	return "unknown";
+}
+
 static void
 hexdump_ppline(int x, uint8_t *data, size_t len)
 {
blob - 78cf40028b8b5683ce038ac20a23e0920073697f
blob + 55b0bbe929a564d5e891da783db89f48371265d8
--- utils.h
+++ utils.h
@@ -27,6 +27,7 @@ char	*xstrdup(const char *);
 void	*xmemdup(const void *, size_t);
 
 const char	*pp_msg_type(uint8_t);
+const char	*pp_qid_type(uint8_t);
 
 void		hexdump(const char *, uint8_t *data, size_t len);