shithub: purgatorio

Download patch

ref: e2e50a8e5a74452672f9418bcdfe4de10f600ed0
parent: 6c2f8fd5e4f0e0be44495a5526930fa3760cd87e
author: henesy <devnull@localhost>
date: Wed Oct 2 18:55:44 EDT 2019

add zipfs(4) and getzip(1) from https://github.com/mjl-/zipfs

--- a/appl/cmd/mkfile
+++ b/appl/cmd/mkfile
@@ -21,6 +21,7 @@
 	spki\
 	ssh\
 	usb\
+	zip\
 
 TARG=\
 	9660srv.dis\
@@ -34,7 +35,7 @@
 	auhdr.dis\
 	basename.dis\
 	bind.dis\
-	# bit2gif.dis\
+	bit2gif.dis\
 	bytes.dis\
 	cal.dis\
 	calc.dis\
--- /dev/null
+++ b/appl/cmd/zip/getzip.b
@@ -1,0 +1,156 @@
+implement Getzip;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "string.m";
+	str: String;
+include "bufio.m";
+include "zip.m";
+	zip: Zip;
+	Fhdr, CDFhdr, Endofcdir: import zip;
+
+Getzip: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+dflag: int;
+kflag: int;
+vflag: int;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	str = load String String->PATH;
+	zip = load Zip Zip->PATH;
+	zip->init();
+
+	# for zip library
+	sys->pctl(Sys->FORKNS, nil);
+	if(sys->bind("#s", "/chan", Sys->MREPL) < 0)
+		fail(sprint("bind /chan: %r"));
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-dkv] zipfile [path ...]");
+	while((c := arg->opt()) != 0)
+		case c {
+		'd' =>	zip->dflag = dflag++;
+		'k' =>	kflag++;
+		'v' =>	vflag++;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args == 0)
+		arg->usage();
+
+	fd := sys->open(hd args, Sys->OREAD);
+	if(fd == nil)
+		fail(sprint("open: %r"));
+
+	(nil, fhdrs, err) := zip->open(fd);
+	if(err != nil)
+		fail("parsing zip: "+err);
+
+	all := tl args == nil;
+	paths: array of string;
+	if(!all) {
+		i := 0;
+		paths = array[len args-1] of string;
+		for(l := tl args; l != nil; l = tl l)
+			paths[i++] = zip->sanitizepath(hd l);
+	}
+File:
+	for(i := 0; i < len fhdrs && (all || len paths > 0 && paths[0] != nil); i++) {
+		if(!all && !match(paths, fhdrs[i].filename))
+			continue;
+
+		f := "./"+fhdrs[i].filename;
+		if(f[len f-1] == '/') {
+			merr := mkdirs(f[2:]);
+			if(merr != nil) {
+				warn(merr);
+				continue;
+			}
+			if(vflag)
+				sys->print("%q\n", fhdrs[i].filename);
+			continue;
+		} else
+			mkdirs(str->splitstrr(f[2:], "/").t0);
+
+		(zfd, nil, zerr) := zip->openfile(fd, fhdrs[i]);
+		if(zerr != nil) {
+			warn(sprint("open %q in zip file: %s", f, zerr));
+			continue;
+		}
+
+		flags := Sys->OWRITE;
+		if(kflag)
+			flags = Sys->OEXCL;
+		ofd := sys->create(f, flags, 8r666);
+		if(ofd == nil) {
+			warn(sprint("create %q: %r", f));
+			continue;
+		}
+
+		buf := array[Sys->ATOMICIO] of byte;
+		for(;;) {
+			n := sys->read(zfd, buf, len buf);
+			if(n < 0) {
+				warn(sprint("reading from %q from zip: %r", f));
+				continue File;
+			}
+			if(n == 0)
+				break;
+			if(sys->write(ofd, buf, n) != n)
+				warn(sprint("writing %q: %r", f));
+		}
+
+		if(vflag)
+			sys->print("%q\n", fhdrs[i].filename);
+	}
+
+	for(i = 0; !all && i < len paths && paths[i] != nil; i++)
+		warn(sprint("path %q not in archive", paths[i++]));
+}
+
+mkdirs(s: string): string
+{
+	p := "";
+	lasterr := 0;
+	for(l := sys->tokenize(s, "/").t1; l != nil; l = tl l) {
+		if(p != nil)
+			p[len p] = '/';
+		p += hd l;
+		lasterr = sys->create(p, Sys->OEXCL|Sys->OREAD, Sys->DMDIR|8r777) == nil;
+	}
+	if(lasterr)
+		return sprint("create ./%q: %r", p);
+	return nil;
+}
+
+match(paths: array of string, s: string): int
+{
+	for(i := 0; i < len paths; i++)
+		if(paths[i] == nil) {
+			return 0;
+		} else if(paths[i] == s) {
+			paths[i:] = paths[i+1:];
+			paths[len paths-1] = nil;
+			return 1;
+		}
+	return 0;
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/zip/lszip.b
@@ -1,0 +1,53 @@
+implement Lszip;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+include "zip.m";
+	zip: Zip;
+	Fhdr, CDFhdr, Endofcdir: import zip;
+
+Lszip: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+dflag: int;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	zip = load Zip Zip->PATH;
+	zip->init();
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-d] file");
+	while((c := arg->opt()) != 0)
+		case c {
+		'd' =>	zip->dflag = dflag++;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+
+	fd := sys->open(hd args, Sys->OREAD);
+	if(fd == nil)
+		fail(sprint("open: %r"));
+
+	(nil, fhdrs, err) := zip->open(fd);
+	if(err != nil)
+		fail("parsing zip: "+err);
+
+	for(i := 0; i < len fhdrs; i++)
+		sys->print("%q\n", fhdrs[i].filename);
+}
+
+fail(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/zip/mkfile
@@ -1,0 +1,28 @@
+<../../../mkconfig
+
+TARG=\
+	lszip.dis\
+	getzip.dis\
+	putzip.dis\
+	zipfs.dis\
+	zipstream.dis\
+
+SYSMODULES=\
+	arg.m\
+	bufio.m\
+	convcs.m\
+	daytime.m\
+	draw.m\
+	encoding.m\
+	filter.m\
+	lists.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+	sys.m\
+	tables.m\
+	zip.m\
+
+DISBIN=$ROOT/dis/zip
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/zip/putzip.b
@@ -1,0 +1,262 @@
+implement Putzip;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "lists.m";
+	lists: Lists;
+include "filter.m";
+	deflate: Filter;
+include "zip.m";
+	zip: Zip;
+	Fhdr, CDFhdr, Endofcdir: import zip;
+
+Putzip: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+dflag: int;
+vflag: int;
+pflag: int;
+
+zb: ref Iobuf;
+fileheaders: list of ref (big, ref Fhdr);
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	lists = load Lists Lists->PATH;
+	deflate = load Filter Filter->DEFLATEPATH;
+	deflate->init();
+	zip = load Zip Zip->PATH;
+	zip->init();
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-dvp] zipfile [path ...]");
+	while((c := arg->opt()) != 0)
+		case c {
+		'd' =>	zip->dflag = dflag++;
+		'v' =>	vflag++;
+		'p' =>	pflag++;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args == 0)
+		arg->usage();
+
+	zfd := sys->create(hd args, Sys->OWRITE|Sys->OEXCL, 8r666);
+	if(zfd == nil)
+		fail(sprint("create %q: %r", hd args));
+	zb = bufio->fopen(zfd, Sys->OWRITE);
+	if(zb == nil)
+		fail(sprint("fopen: %r"));
+
+	args = tl args;
+	if(args == nil) {
+		b := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		if(b == nil)
+			fail(sprint("fopen: %r"));
+		for(;;) {
+			s := b.gets('\n');
+			if(s == nil)
+				break;
+			if(s != nil && s[len s-1] == '\n')
+				s = s[:len s-1];
+			put(s, 0);
+		}
+	} else {
+		for(; args != nil; args = tl args)
+			put(hd args, 1);
+	}
+
+	eocd := ref Endofcdir (0, 0, len fileheaders, len fileheaders, big 0, big 0, nil);
+	eocd.cdiroffset = zb.offset();
+	for(l := lists->reverse(fileheaders); l != nil; l = tl l) {
+		(foff, f) := *hd l;
+		cdf := CDFhdr.mk(f, foff);
+		buf := cdf.pack();
+		if(zb.write(buf, len buf) != len buf)
+			fail(sprint("writing central directory file header: %r"));
+		eocd.cdirsize += big len buf;
+	}
+
+	ebuf := eocd.pack();
+	if(zb.write(ebuf, len ebuf) != len ebuf || zb.flush() == Bufio->ERROR)
+		fail(sprint("writing end of central directory: %r"));
+}
+
+put(s: string, recur: int)
+{
+	if(s == nil)
+		warn("refusing to add empty filename");
+
+	fd := sys->open(s, Sys->OREAD);
+	if(fd == nil)
+		return warn(sprint("open %q: %r, skipping", s));
+	(ok, dir) := sys->fstat(fd);
+	if(ok < 0)
+		return warn(sprint("fstat %q: %r, skipping", s));
+
+	if(dir.mode & Sys->DMDIR)
+		putdir(s, fd, dir, recur);
+	else
+		putfile(s, fd, dir);
+}
+
+mkfhdr(mtime: int, s: string): ref Fhdr
+{
+	f := ref Fhdr;
+	f.versneeded = 20;
+	f.flags = zip->Futf8;
+	f.comprmethod = zip->Mdeflate;
+	if(pflag)
+		f.comprmethod = zip->Mplain;
+	f.mtime = mtime;
+	f.filename = zip->sanitizepath(s);
+	f.comprsize = big 0;
+	f.uncomprsize = big 0;
+	f.crc32 = big 0;
+	return f;
+}
+
+putdir(s: string, fd: ref Sys->FD, dir: Sys->Dir, recur: int)
+{
+	if(s[len s-1] != '/')
+		s[len s] = '/';
+	f := mkfhdr(dir.mtime, s);
+	foff := zb.offset();
+	fbuf := f.pack();
+	if(zb.write(fbuf, len fbuf) != len fbuf)
+		fail(sprint("write: %r"));
+	fileheaders = ref (foff, f)::fileheaders;
+	if(vflag)
+		sys->print("%s\n", s);
+
+	if(!recur)
+		return;
+
+	for(;;) {
+		(n, dirs) := sys->dirread(fd);
+		if(n < 0)
+			return warn(sprint("listing %q: %r", s));
+		if(n == 0)
+			break;
+		for(i := 0; i < len dirs; i++)
+			put(s+dirs[i].name, recur);
+	}
+}
+
+putfile(s: string, fd: ref Sys->FD, dir: Sys->Dir)
+{
+	f := mkfhdr(dir.mtime, s);
+	if(vflag)
+		sys->print("%s\n", s);
+
+	foff := zb.offset();
+
+	# write partially filled header, prevents fs holes
+	fbuf := f.pack();
+	if(zb.write(fbuf, len fbuf) != len fbuf)
+		fail(sprint("write: %r"));
+
+	if(f.comprmethod == zip->Mplain)
+		putplain(fd, f);
+	else
+		putdeflate(fd, f, s);
+
+	# rewrite file header, now complete.  restore offset afterwards
+	off := zb.offset();
+	fbuf = f.pack();
+	if(zb.seek(foff, Bufio->SEEKSTART) < big 0)
+		fail(sprint("seek to file header: %r"));
+	if(zb.write(fbuf, len fbuf) != len fbuf)
+		fail(sprint("write %q file header: %r", s));
+	if(zb.seek(off, Bufio->SEEKSTART) < big 0)
+		fail(sprint("seek to past compressed contents: %r"));
+
+	fileheaders = ref (foff, f)::fileheaders;
+}
+
+putplain(fd: ref Sys->FD, f: ref Fhdr)
+{
+	crc := ~0;
+
+	buf := array[sys->ATOMICIO] of byte;
+	for(;;) {
+		n := sys->read(fd, buf, len buf);
+		if(n == 0)
+			break;
+		if(n < 0)
+			fail(sprint("read: %r"));
+		if(zb.write(buf, n) != n)
+			fail(sprint("write: %r"));
+		crc = zip->crc32(crc, buf[:n]);
+		f.uncomprsize += big n;
+	}
+	f.comprsize = f.uncomprsize;
+	f.crc32 = big ~crc;
+}
+
+putdeflate(fd: ref Sys->FD, f: ref Fhdr, s: string)
+{
+	rqc := deflate->start("");
+	pick r := <-rqc {
+	Start =>	;
+	* =>	fail(sprint("bad first filter msg"));
+	}
+
+	crc := ~0;
+Filter:
+	for(;;) pick rq := <-rqc {
+	Fill =>
+		n := sys->read(fd, rq.buf, len rq.buf);
+		if(n >= 0)
+			crc = zip->crc32(crc, rq.buf[:n]); 
+		rq.reply <-= n;
+		if(n < 0)
+			fail(sprint("reading %q: %r", s));
+		f.uncomprsize += big n;
+	Result =>
+		if(zb.write(rq.buf, len rq.buf) != len rq.buf)
+			fail(sprint("writing %q compressed: %r", s));
+		f.comprsize += big len rq.buf;
+		rq.reply <-= 0;
+	Finished =>
+		if(len rq.buf != 0)
+			fail(sprint("deflate leftover bytes..."));
+		break Filter;
+	Info =>
+		say("deflate: "+rq.msg);
+	Error =>
+		fail("deflate error: "+rq.e);
+	}
+
+	f.crc32 = big ~crc;
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/zip/zipfs.b
@@ -1,0 +1,321 @@
+# zipfs design
+# 
+# at startup we read the entire central directory,
+# set up a styxservers nametree and serve from that.
+# bit 62 of the qid denotes the directory bit.
+#
+# if a file is not compressed and -p was set, we read directly through
+# the zipfile's fd (no crc protection).
+# otherwise, on first read of a file, we open a fd for that file.
+# sequential or higher-than-current offset reads reuse the fd.
+# random reads to before the current offset reopen the file and
+# seek to the desired location by reading & discarding data.
+
+implement Zipfs;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "daytime.m";
+	daytime: Daytime;
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import Styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Fid, Navigator, Navop: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+include "tables.m";
+	tables: Tables;
+	Table, Strhash: import tables;
+include "zip.m";
+	zip: Zip;
+	Fhdr, CDFhdr, Endofcdir: import zip;
+
+Zipfs: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+
+# directories for styx nametree
+Zdir: adt {
+	name:	string;
+	qid:	big;
+	pqid:	big;
+};
+
+# opened file from zip archive
+Zfile: adt {
+	fid:	int;
+	fd:	ref Sys->FD;
+	off:	big;
+	cdf:	ref CDFhdr;
+	f:	ref Fhdr;
+};
+
+Qiddir: con big 1<<62;
+
+zipfd: ref Sys->FD;
+srv: ref Styxserver;
+files: array of ref Fhdr;
+zdirs: ref Strhash[ref Zdir]; # indexed by full path without trailing slash
+rootzdir: ref Zdir;
+dirgen: int;
+
+cdirfhdrs: array of ref CDFhdr;
+
+zfiles: ref Table[ref Zfile]; # indexed by fid
+
+Dflag: int;
+dflag: int;
+pflag: int;
+now: int;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	daytime = load Daytime Daytime->PATH;
+	styx = load Styx Styx->PATH;
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	styxservers->init(styx);
+	nametree = load Nametree Nametree->PATH;
+	nametree->init();
+	tables = load Tables Tables->PATH;
+	zip = load Zip Zip->PATH;
+	zip->init();
+
+	# for zip library
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+	if(sys->bind("#s", "/chan", Sys->MREPL) < 0)
+		fail(sprint("bind /chan: %r"));
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-Ddp] zipfile");
+	while((c := arg->opt()) != 0)
+		case c {
+		'D' =>	styxservers->traceset(Dflag++);
+		'd' =>	zip->dflag = dflag++;
+		'p' =>	pflag++;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+
+	zdirs = zdirs.new(101, nil);
+
+	zipfd = sys->open(hd args, Sys->OREAD);
+	if(zipfd == nil)
+		fail(sprint("open: %r"));
+
+	err: string;
+	(nil, cdirfhdrs, err) = zip->open(zipfd);
+	if(err != nil)
+		fail("parsing zip: "+err);
+
+	now = daytime->now();
+	(tree, treeop) := nametree->start();
+	rzd := rootzdir = ref Zdir (".", big 0|Qiddir, big 0|Qiddir);
+	tree.create(rzd.pqid, dir(rzd.name, rzd.qid, now, big 0));
+
+	for(i := 0; i < len cdirfhdrs; i++) {
+		f := cdirfhdrs[i];
+		fs := f.filename;
+		(path, name) := str->splitstrr(fs, "/");
+		zd := ensuredir(tree, path);
+		if(fs == nil || fs[len fs-1] == '/')
+			continue;
+		qid := big (i+1);
+		tree.create(zd.qid, dir(name, qid, f.mtime, f.uncomprsize));
+	}
+	zdirs = nil;
+
+	zfiles = zfiles.new(31, nil);
+
+	msgc: chan of ref Tmsg;
+	(msgc, srv) = Styxserver.new(sys->fildes(0), Navigator.new(treeop), big 0|Qiddir);
+	spawn styxsrv(msgc);
+}
+
+# inefficient
+# called with empty string (denoting the root directory), or path with trailing slash
+ensuredir(tree: ref Tree, s: string): ref Zdir
+{
+	if(s == nil)
+		return rootzdir;
+	s = s[:len s-1];
+
+	zd := zdirs.find(s);
+	if(zd != nil)
+		return zd;
+
+	(ppath, name) := str->splitstrr(s, "/");
+	pzd := ensuredir(tree, ppath);
+	zd = ref Zdir (name, big ++dirgen|Qiddir, pzd.qid);
+	tree.create(pzd.qid, dir(zd.name, zd.qid, now, big 0));
+	zdirs.add(s, zd);
+	return zd;
+}
+
+dir(name: string, qid: big, mtime: int, length: big): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = "zip";
+	d.gid = "zip";
+	d.qid.path = qid;
+	if((qid & Qiddir) != big 0) {
+		d.qid.qtype = Sys->QTDIR;
+		d.mode = Sys->DMDIR|8r555;
+	} else {
+		d.qid.qtype = Sys->QTFILE;
+		d.mode = 8r444;
+	}
+	d.mtime = d.atime = mtime;
+	d.length = length;
+	return d;
+}
+
+styxsrv(msgc: chan of ref Tmsg)
+{
+done:
+	for(;;) alt {
+	mm := <-msgc =>
+		if(mm == nil)
+			break done;
+		pick m := mm {
+		Readerror =>
+			warn("read error: "+m.error);
+			break done;
+		}
+		dostyx(mm);
+	}
+	killgrp(sys->pctl(0, nil));
+}
+
+openzfile(fid: int, qid: big): (ref Zfile, string)
+{
+	cdf := cdirfhdrs[int qid-1];
+	zf := ref Zfile (fid, nil, big 0, cdf, nil);
+	err: string;
+	if(pflag && cdf.comprmethod == zip->Mplain)
+		(zf.f, err) = zip->readfhdr(zipfd, cdf);
+	else
+		(zf.fd, nil, err) = zip->openfile(zipfd, cdf);
+	if(err != nil)
+		return (nil, err);
+	if(Dflag) warn("zipfs: "+cdf.filename);
+	zfiles.add(fid, zf);
+	return (zf, nil);
+}
+
+zfileseek(zf: ref Zfile, off: big): string
+{
+	if(zf.off == off)
+		return nil;
+
+	if(off < zf.off) {
+		(fd, nil, err) := zip->openfile(zipfd, zf.cdf);
+		if(err != nil)
+			return err;
+		zf.fd = fd;
+		zf.off = big 0;
+	}
+
+	n := int (off-zf.off);
+	buf := array[Sys->ATOMICIO] of byte;
+	while(n > 0) {
+		nn := sys->read(zf.fd, buf, len buf);
+		if(nn < 0)
+			return sprint("seeking to requested offset: %r");
+		if(nn == 0)
+			break;  # not there yet, but subsequent reads on zf.fd will/should return eof too
+		zf.off += big nn;
+		n -= nn;
+	}
+
+	return nil;
+}
+
+dostyx(mm: ref Tmsg)
+{
+	pick m := mm {
+	Clunk or
+	Remove =>
+		f := srv.getfid(m.fid);
+		if(f != nil && f.isopen && (f.path & Qiddir) == big 0)
+			zfiles.del(m.fid);
+		srv.default(m);
+
+	Read =>
+		f := srv.getfid(m.fid);
+		if(f.qtype & Sys->QTDIR)
+			return srv.default(m);
+
+		zf := zfiles.find(m.fid);
+		if(zf == nil) {
+			err: string;
+			(zf, err) = openzfile(m.fid, f.path);
+			if(err != nil)
+				return replyerror(m, err);
+		}
+
+		if(zf.f == nil) {
+			err := zfileseek(zf, m.offset);
+			if(err != nil)
+				return replyerror(m, err);
+			n := sys->read(zf.fd, buf := array[m.count] of byte, len buf);
+			if(n < 0)
+				return replyerror(m, sprint("%r"));
+			zf.off += big n;
+			srv.reply(ref Rmsg.Read(m.tag, buf[:n]));
+		} else {
+			n := zip->pread(zipfd, zf.f, buf := array[m.count] of byte, len buf, m.offset);
+			if(n < 0)
+				return replyerror(m, sprint("%r"));
+			srv.reply(ref Rmsg.Read(m.tag, buf[:n]));
+		}
+
+	* =>
+		srv.default(mm);
+	}
+}
+
+replyerror(m: ref Tmsg, s: string)
+{
+	srv.reply(ref Rmsg.Error(m.tag, s));
+}
+
+killgrp(pid: int)
+{
+	sys->fprint(sys->open(sprint("/prog/%d/ctl", pid), Sys->OWRITE), "killgrp");
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/zip/zipstream.b
@@ -1,0 +1,209 @@
+# rewrite zip file, putting central directory at the front of the file.
+
+implement Zipstream;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "filter.m";
+	deflate: Filter;
+include "zip.m";
+	zip: Zip;
+	Fhdr, CDFhdr, Endofcdir: import zip;
+
+Zipstream: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+dflag: int;
+oflag: int;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	deflate = load Filter Filter->DEFLATEPATH;
+	deflate->init();
+	zip = load Zip Zip->PATH;
+	zip->init();
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-d] [-o] zipfile");
+	while((c := arg->opt()) != 0)
+		case c {
+		'd' =>	zip->dflag = dflag++;
+		'o' =>	oflag++;
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+
+	fd := sys->open(hd args, Sys->OREAD);
+	if(fd == nil)
+		fail(sprint("open: %r"));
+
+	(eocd, cdfhdrs, err) := zip->open(fd);
+	if(err != nil)
+		fail("parsing zip: "+err);
+
+	if(oflag) {
+		files := readfiles();
+		n := array[len cdfhdrs] of ref CDFhdr;
+		for(i := 0; i < len files; i++) {
+			j := find(cdfhdrs, files[i]);
+			if(j < 0)
+				fail(sprint("%#q not in zip file or specified twice", files[i]));
+			n[i] = cdfhdrs[j];
+			cdfhdrs[j] = nil;
+		}
+		for(j := 0; j < len cdfhdrs; j++)
+			if(cdfhdrs[j] != nil)
+				n[i++] = cdfhdrs[j];
+		cdfhdrs = n;
+	}
+
+	zb := bufio->fopen(sys->fildes(1), bufio->OWRITE);
+	if(zb == nil)
+		fail(sprint("fopen: %r"));
+
+	# calculate size of cdfhdrs
+	o := big 0;
+	for(i := 0; i < len cdfhdrs; i++)
+		o += big len cdfhdrs[i].pack();
+	cdsz := o;
+
+	eocd.cdirsize = cdsz;
+	eocd.cdiroffset = big 0;
+	o += big len eocd.pack();
+
+	# fix the offsets in the cdfhdrs & fhdrs, store original offsets
+	origoff := array[len cdfhdrs] of big;
+	fhdrs := array[len cdfhdrs] of ref Fhdr;
+	for(i = 0; i < len fhdrs; i++) {
+		cdf := cdfhdrs[i];
+		f: ref Fhdr;
+		(f, err) = Fhdr.read(fd, cdf.reloffset);
+		if(err != nil)
+			fail("reading local file header: "+err);
+		origoff[i] = f.dataoff;
+		cdf.reloffset = o;
+		fhdrs[i] = Fhdr.mk(cdf);
+		o += big len fhdrs[i].pack()+fhdrs[i].comprsize;
+	}
+
+	# write the cdfhdrs
+	for(i = 0; i < len cdfhdrs; i++) {
+		if(zb.write(buf := cdfhdrs[i].pack(), len buf) != len buf)
+			fail(sprint("write: %r"));
+	}
+	# a copy of the end of central directory structure (to keep other zip code happy)
+	if(zb.write(buf := eocd.pack(), len buf) != len buf)
+		fail(sprint("write: %r"));
+
+	# write the fhdrs & file contents
+	for(i = 0; i < len fhdrs; i++) {
+		if(zb.write(buf = fhdrs[i].pack(), len buf) != len buf || copyrange(fd, origoff[i], fhdrs[i].comprsize, zb) < 0)
+			fail(sprint("write: %r"));
+	}
+
+	if(zb.offset() != o)
+		fail(sprint("inconsitent offset after rewriting central directory and files, expected offset %bd, saw %bd", o, zb.offset()));
+
+	# write the eocd, this one is typically found by code parsing the zip file
+	if(zb.write(buf = eocd.pack(), len buf) != len buf || zb.flush() == bufio->ERROR)
+		fail(sprint("write: %r"));
+}
+
+readfiles(): array of string
+{
+	b := bufio->fopen(sys->fildes(0), bufio->OREAD);
+	if(b == nil)
+		fail(sprint("fopen: %r"));
+	l: list of string;
+	for(;;) {
+		s := b.gets('\n');
+		if(s == nil)
+			break;
+		if(s[len s-1] == '\n')
+			s = s[:len s-1];
+		l = s::l;
+	}
+	f := array[len l] of string;
+	i := len f-1;
+	for(; l != nil; l = tl l)
+		f[i--] = hd l;
+	return f;
+}
+
+find(h: array of ref CDFhdr, s: string): int
+{
+	for(i := 0; i < len h; i++)
+		if(h[i] != nil && h[i].filename == s)
+			return i;
+	return -1;
+}
+
+copyrange(fd: ref Sys->FD, off: big, n: big, zb: ref Iobuf): int
+{
+	buf := array[sys->ATOMICIO] of byte;
+	while(n > big 0) {
+		if(n > big len buf)
+			nn := len buf;
+		else
+			nn = int n;
+		r := preadn(fd, buf, nn, off);
+		if(r < 0)
+			return -1;
+		else if(r != nn) {
+			sys->werrstr("short read");
+			return -1;
+		}
+		if(zb.write(buf, r) != r)
+			return -1;
+		off += big r;
+		n -= big r;
+	}
+	return 0;
+}
+
+preadn(fd: ref Sys->FD, buf: array of byte, n: int, off: big): int
+{
+	org := n;
+	while(n > 0) {
+		nn := sys->pread(fd, buf, n, off);
+		if(nn < 0)
+			return nn;
+		if(nn == 0)
+			break;
+		n -= nn;
+		off += big nn;
+		buf = buf[nn:];
+	}
+	return org-n;
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- a/appl/lib/mkfile
+++ b/appl/lib/mkfile
@@ -155,6 +155,7 @@
 	workdir.dis\
 	writegif.dis\
 	xml.dis\
+	zip.dis\
 
 MODULES=
 
--- /dev/null
+++ b/appl/lib/zip.b
@@ -1,0 +1,917 @@
+implement Zip;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "daytime.m";
+	dt: Daytime;
+include "encoding.m";
+	base16: Encoding;
+include "filter.m";
+	inflate: Filter;
+include "convcs.m";
+	convcs: Convcs;
+	cp437: Btos;
+include "zip.m";
+
+comprmethods := array[] of {
+	"uncompressed",
+	"shrunk",
+	"reduce, factor 1",
+	"reduce, factor 2",
+	"reduce, factor 3",
+	"reduce, factor 4",
+	"implode",
+	"tokenize",
+	"deflate",
+	"deflate64",
+	"pkware implode",
+	nil,
+	"bzip2",
+	nil,
+	"lzma",
+Mibmterse =>
+	"ibm terse (new)",
+Mlz77z =>
+	"ibm lz77 z",
+Mwavpack =>
+	"wavpack",
+Mppmdi1 =>
+	"PPMd version I, rev 1",
+};
+
+Crc32poly: con int 16redb88320;  # reversed standard crc-32
+crc32tab: array of int;
+
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	base16 = load Encoding Encoding->BASE16PATH;
+	dt = load Daytime Daytime->PATH;
+	inflate = load Filter Filter->INFLATEPATH;
+	inflate->init();
+	convcs = load Convcs Convcs->PATH;
+	err := convcs->init(nil);
+	if(err == nil)
+		(cp437, err) = convcs->getbtos("cp437");
+	if(err != nil)
+		err = "convcs: "+err;
+
+	crc32tab = mkcrctab(Crc32poly);
+
+	return err;
+}
+
+
+sanitizepath(s: string): string
+{
+	endslash := s != nil && s[len s-1] == '/';
+	r: list of string;
+	for(l := sys->tokenize(s, "/").t1; l != nil; l = tl l)
+		case hd l {
+		"." =>	;
+		".." =>	if(r != nil) r = tl r;
+		* =>	r = hd l::r;
+		}
+	rs := "";
+	for(r = rev(r); r != nil; r = tl r)
+		rs += "/"+hd r;
+	if(rs != nil)
+		rs = rs[1:];
+	if(endslash)
+		rs[len rs] = '/';
+	return rs;
+}
+
+Extra.parse(d: array of byte): (ref Extra, string)
+{
+	e := ref Extra;
+	{
+		o := 0;
+		while(o < len d) {
+			id: int;
+			buf: array of byte;
+			(id, o) = g16(d, o);
+			(buf, o) = gstr(d, o);
+			e.l = ref (id, buf)::e.l;
+		}
+	} exception {
+	"get:*" =>
+		return (nil, sprint("bad extra"));
+	}
+	e.l = rev(e.l);
+	return (e, nil);
+}
+
+Extra.pack(e: self ref Extra): array of byte
+{
+	n := 0;
+	if(e != nil)
+		l := e.l;
+	for(t := l; t != nil; t = tl t)
+		n += 2+2+len (hd t).t1;
+	buf := array[n] of byte;
+	o := 0;
+	for(; l != nil; l = tl l) {
+		(id, dat) := *hd l;
+		o = p16(buf, o, id);
+		o = p16(buf, o, len dat);
+		o = pbuf(buf, o, dat);
+	}
+	return buf;
+}
+
+Extra.text(e: self ref Extra): string
+{
+	s: string;
+	for(l := e.l; l != nil; l = tl l)
+		s += sprint(", 0x%04ux=%s", (hd l).t0, base16->enc((hd l).t1));
+	if(s != nil)
+		s = s[2:];
+	return "Extra("+s+")";
+}
+
+
+Fhdr.mk(x: ref CDFhdr): ref Fhdr
+{
+	return ref Fhdr (
+		x.versneeded,
+		x.flags,
+		x.comprmethod,
+		x.filemtime,
+		x.filemdate,
+		x.mtime,
+		x.crc32,
+		x.comprsize,
+		x.uncomprsize,
+		x.filename,
+		ref *x.extra,
+		big 0);
+}
+
+fhdrsig := array[] of {byte 'P', byte 'K', byte 3, byte 4};
+Fhdr.parse(buf: array of byte, off: big): (ref Fhdr, string)
+{
+	if(len buf < 4 || bufcmp(buf[:4], fhdrsig) != 0)
+		return (nil, sprint("not a local file header"));
+	o := 4;
+	f := ref Fhdr;
+	{
+		(f.versneeded, o) = g16(buf, o);
+		(f.flags, o) = g16(buf, o);
+		(f.comprmethod, o) = g16(buf, o);
+		(f.filemtime, o) = g16(buf, o);
+		(f.filemdate, o) = g16(buf, o);
+		f.mtime = mtimedos2unix(f.filemtime, f.filemdate);
+		(f.crc32, o) = g32(buf, o);
+		(f.comprsize, o) = g32(buf, o);
+		(f.uncomprsize, o) = g32(buf, o);
+		flen, extralen: int;
+		(flen, o) = g16(buf, o);
+		(extralen, o) = g16(buf, o);
+		(f.filename, o) = gbufstr(f.flags&Futf8, flen, buf, o);
+		f.filename = sanitizepath(f.filename);
+		extra: array of byte;
+		(extra, o) = gbuf(extralen, buf, o);
+		err: string;
+		(f.extra, err) = Extra.parse(extra);
+		if(err != nil)
+			return (nil, "bad extra for local file header: "+err);
+		f.dataoff = off+big o;
+	} exception {
+	"get:*" =>
+		return (nil, sprint("short buffer for local file header (o %d, len %d)", o, len buf));
+	}
+	return (f, nil);
+}
+
+Fhdr.read(fd: ref Sys->FD, off: big): (ref Fhdr, string)
+{
+	minwidth: con 4+5*2+3*4+2*2;
+	n := preadn(fd, buf0 := array[minwidth] of byte, len buf0, off);
+	if(n < 0)
+		return (nil, sprint("read: %r"));
+	if(n != len buf0)
+		return (nil, "short read");
+	flen := g16(buf0, len buf0-4).t0;
+	extralen := g16(buf0, len buf0-2).t0;
+	buf1 := array[flen+extralen] of byte;
+	if(len buf1 > 0) {
+		n = preadn(fd, buf1, len buf1, off+big minwidth);
+		if(n < 0)
+			return (nil, sprint("read: %r"));
+		if(n != len buf1)
+			return (nil, "short read");
+	}
+	buf := array[len buf0+len buf1] of byte;
+	buf[:] = buf0;
+	buf[len buf0:] = buf1;
+	return Fhdr.parse(buf, off);
+}
+
+Fhdr.pack(f: self ref Fhdr): array of byte
+{
+	filename := array of byte f.filename;
+	ebuf := f.extra.pack();
+	buf := array[4+5*2+3*4+2+len filename+2+len ebuf] of byte;
+
+	(f.filemtime, f.filemdate) = mtimeunix2dos(f.mtime);
+
+	o := 0;
+	o = pbuf(buf, o, fhdrsig);
+	o = p16(buf, o, f.versneeded);
+	o = p16(buf, o, f.flags);
+	o = p16(buf, o, f.comprmethod);
+	o = p16(buf, o, f.filemtime);
+	o = p16(buf, o, f.filemdate);
+	o = p32(buf, o, f.crc32);
+	o = p32(buf, o, f.comprsize);
+	o = p32(buf, o, f.uncomprsize);
+	o = p16(buf, o, len filename);
+	o = p16(buf, o, len ebuf);
+	o = pbuf(buf, o, filename);
+	o = pbuf(buf, o, ebuf);
+	return buf;
+}
+
+Fhdr.text(f: self ref Fhdr): string
+{
+	return sprint("Fhdr(versneeded %d (%s), flags %ux, comprmethod %d/%s, mtime %d, crc32 %bd, comprsize %bd, uncomprsize %bd, filename %q, %s, dataoff %bd)",
+		f.versneeded, versstr(f.versneeded),
+		f.flags,
+		f.comprmethod, comprmethod(f.comprmethod),
+		f.mtime,
+		f.crc32,
+		f.comprsize, f.uncomprsize,
+		f.filename,
+		f.extra.text(),
+		f.dataoff);
+}
+
+
+mask(n: int): int
+{
+	return (1<<n)-1;
+}
+
+mtimeunix2dos(m: int): (int, int)
+{
+	tm := dt->local(m);
+	s := tm.sec | tm.min<<5 | tm.hour<<11;
+	d := tm.mday | (tm.mon+1)<<5 | (tm.year-80)<<9;
+	return (s, d);
+}
+
+zerotm: ref dt->Tm;
+mtimedos2unix(s, d: int): int
+{
+	if(zerotm == nil)
+		zerotm = dt->local(dt->now());
+	tm := ref *zerotm;
+	tm.sec	= (s>>0) & mask(5);
+	tm.min	= (s>>5) & mask(6);
+	tm.hour	= (s>>11) & mask(5);
+	tm.mday	= (d>>0) & mask(5);
+	tm.mon	= ((d>>5) & mask(4))-1;
+	tm.year	= ((d>>9) & mask(7))+80;
+	tm.wday	= 0;
+	tm.yday	= 0;
+	return dt->tm2epoch(tm);
+}
+
+CDFhdr.mk(f: ref Fhdr, off: big): ref CDFhdr
+{
+	return ref CDFhdr (
+		f.versneeded,  # we're not claiming to be unix, because then unzip sets 0 for permissions when absent
+                f.versneeded,
+		f.flags,
+		f.comprmethod,
+		f.filemtime,
+		f.filemdate,
+		f.mtime,
+		f.crc32,
+		f.comprsize,
+		f.uncomprsize,
+		f.filename,
+		f.extra,
+		nil,
+		0,
+		0,
+		big 0,
+		off);
+}
+
+cdirfhdrsig := array[] of {byte 'P', byte 'K', byte 1, byte 2};
+
+CDFhdr.parse(buf: array of byte): (ref CDFhdr, string)
+{
+	if(len buf < 4 || bufcmp(buf[:4], cdirfhdrsig) != 0)
+		return (nil, "not a central directory file header");
+	f := ref CDFhdr;
+	o := 4;
+	{
+		(f.versmadeby, o) = g16(buf, o);
+		(f.versneeded, o) = g16(buf, o);
+		(f.flags, o) = g16(buf, o);
+		if(f.flags & Fcdirencrypted)
+			return (nil, "central directory is encrypted, not supported");
+		(f.comprmethod, o) = g16(buf, o);
+		(f.filemtime, o) = g16(buf, o);
+		(f.filemdate, o) = g16(buf, o);
+		f.mtime = mtimedos2unix(f.filemtime, f.filemdate);
+		(f.crc32, o) = g32(buf, o);
+		(f.comprsize, o) = g32(buf, o);
+		(f.uncomprsize, o) = g32(buf, o);
+		flen, extralen, commentlen: int;
+		(flen, o) = g16(buf, o);
+		(extralen, o) = g16(buf, o);
+		(commentlen, o) = g16(buf, o);
+		(f.disknrstart, o) = g16(buf, o);
+		(f.intattr, o) = g16(buf, o);
+		(f.extattr, o) = g32(buf, o);
+		(f.reloffset, o) = g32(buf, o);
+		(f.filename, o) = gbufstr(f.flags&Futf8, flen, buf, o);
+		f.filename = sanitizepath(f.filename);
+		extra: array of byte;
+		(extra, o) = gbuf(extralen, buf, o);
+		err: string;
+		(f.extra, err) = Extra.parse(extra);
+		if(err != nil)
+			return (nil, sprint("bad extra for central directory file header"));
+		(f.comment, o) = gbufstr(f.flags&Futf8, commentlen, buf, o);
+		if(o != len buf)
+			say(sprint("%d trailing bytes after parsing central directory file header", len buf-o));
+	} exception {
+	"get:*" =>
+		return (nil, sprint("short buffer for central directory file header (o %d, len %d)", o, len buf));
+	}
+	return (f, nil);
+}
+
+CDFhdr.read(b: ref Bufio->Iobuf): (ref CDFhdr, string)
+{
+	buf0 := array[4+6*2+3*4+5*2+2*4] of byte;
+	if(breadn(b, buf0, len buf0) != len buf0)
+		return (nil, sprint("short read on central directory file header"));
+	if(bufcmp(buf0[:4], cdirfhdrsig) != 0)
+		return (nil, sprint("not signature of central directory file header"));
+	lenoff := 4+6*2+3*4;
+	n := g16(buf0, lenoff).t0;
+	n += g16(buf0, lenoff+2).t0;
+	n += g16(buf0, lenoff+4).t0;
+	buf1 := array[n] of byte;
+	if(breadn(b, buf1, len buf1) != len buf1)
+		return (nil, sprint("short read on filename/extra/comment section of central directory file header"));
+	buf := array[len buf0+len buf1] of byte;
+	buf[:] = buf0;
+	buf[len buf0:] = buf1;
+	return CDFhdr.parse(buf);
+}
+
+CDFhdr.pack(f: self ref CDFhdr): array of byte
+{
+	filename := array of byte f.filename;
+	comment := array of byte f.comment;
+	ebuf := f.extra.pack();
+	buf := array[4+6*2+3*4+2+len filename+2+len ebuf+2+len comment+2*2+2*4] of byte;
+
+	(f.filemtime, f.filemdate) = mtimeunix2dos(f.mtime);
+
+	o := 0;
+	o = pbuf(buf, o, cdirfhdrsig);
+	o = p16(buf, o, f.versmadeby);
+	o = p16(buf, o, f.versneeded);
+	o = p16(buf, o, f.flags);
+	o = p16(buf, o, f.comprmethod);
+	o = p16(buf, o, f.filemtime);
+	o = p16(buf, o, f.filemdate);
+	o = p32(buf, o, f.crc32);
+	o = p32(buf, o, f.comprsize);
+	o = p32(buf, o, f.uncomprsize);
+	o = p16(buf, o, len filename);
+	o = p16(buf, o, len ebuf);
+	o = p16(buf, o, len comment);
+	o = p16(buf, o, f.disknrstart);
+	o = p16(buf, o, f.intattr);
+	o = p32(buf, o, f.extattr);
+	o = p32(buf, o, f.reloffset);
+	o = pbuf(buf, o, filename);
+	o = pbuf(buf, o, ebuf);
+	o = pbuf(buf, o, comment);
+	return buf;
+}
+
+CDFhdr.text(f: self ref CDFhdr): string
+{
+	return sprint("CDFhdr(version: madeby %d (%s), needed %d (%s); flags %02ux, comprmethod %d/%s, mtime %d, crc32 %bux, comprsize %bd, uncomprsize %bd, file %q, %s, comment %q, disknrstart %d, intattr %02x, extattr %04bux, reloffset %bd)",
+		f.versmadeby, versstr(f.versmadeby),
+		f.versneeded, versstr(f.versneeded),
+		f.flags, f.comprmethod, comprmethod(f.comprmethod),
+		f.mtime, f.crc32, f.comprsize, f.uncomprsize,
+		f.filename,
+		f.extra.text(),
+		f.comment,
+		f.disknrstart,
+		f.intattr, f.extattr, f.reloffset);
+}
+
+
+eocentraldirsig := array[] of {byte 'P', byte 'K', byte 5, byte 6};
+Endofcdir.parse(buf: array of byte): (ref Endofcdir, string)
+{
+	e := ref Endofcdir;
+	if(len buf < 4 || bufcmp(buf[:4], eocentraldirsig) != 0)
+		return (nil, "not end of central directory");
+	o := 4;
+	{
+		(e.disknr, o) = g16(buf, o);
+		(e.diskcdir, o) = g16(buf, o);
+		(e.diskcdirentries, o) = g16(buf, o);
+		(e.cdirentries, o) = g16(buf, o);
+		(e.cdirsize, o) = g32(buf, o);
+		(e.cdiroffset, o) = g32(buf, o);
+		(e.comment, o) = gstr(buf, o);
+		if(o != len buf)
+			say(sprint("%d trailing bytes after end of central directory", len buf-o));
+	} exception {
+	"get:*" =>
+		return (nil, sprint("short buffer for end of central directory buffer, (o %d, len %d)", o, len buf));
+	}
+	return (e, nil);
+}
+
+Endofcdir.pack(e: self ref Endofcdir): array of byte
+{
+	buf := array[4+4*2+2*4+2+len e.comment] of byte;
+	o := 0;
+	o = pbuf(buf, o, eocentraldirsig);
+	o = p16(buf, o, e.disknr);
+	o = p16(buf, o, e.diskcdir);
+	o = p16(buf, o, e.diskcdirentries);
+	o = p16(buf, o, e.cdirentries);
+	o = p32(buf, o, e.cdirsize);
+	o = p32(buf, o, e.cdiroffset);
+	o = p16(buf, o, len e.comment);
+	o = pbuf(buf, o, e.comment);
+	return buf;
+}
+
+Endofcdir.text(e: self ref Endofcdir): string
+{
+	return sprint("Endofcdir(disk: nr %d, cdir %d, cdirentries %d;  cdir: entries %d, size %bd, offset %bd;  comment %q)",
+		e.disknr, e.diskcdir, e.diskcdirentries, e.cdirentries, e.cdirsize, e.cdiroffset, string e.comment);
+}
+
+
+comprmethod(m: int): string
+{
+	if(m < 0 || m >= len comprmethods || comprmethods[m] == nil)
+		return "unknown";
+	return comprmethods[m];
+}
+
+open(fd: ref Sys->FD): (ref Endofcdir, array of ref CDFhdr, string)
+{
+	{
+		return open0(fd);
+	} exception e {
+	"open0:*" =>
+		return (nil, nil, e[len "open0:":]);
+	}
+}
+
+error(s: string)
+{
+	raise "open0:"+s;
+}
+
+open0(fd: ref Sys->FD): (ref Endofcdir, array of ref CDFhdr, string)
+{
+	(ok, dir) := sys->fstat(fd);
+	if(ok < 0)
+		error(sprint("stat: %r"));
+	size := dir.length;
+	off := size-big (8*1024);
+	if(off < big 0)
+		off = big 0;
+	n := preadn(fd, buf := array[int (size-off)] of byte, len buf, off);
+	if(n < 0)
+		error(sprint("read: %r"));
+	buf = buf[:n];
+	(o, eerr) := findeocdir(buf);
+	if(eerr != nil)
+		error("cannot parse file: "+eerr);
+	off += big o;
+
+	(eocdir, err) := Endofcdir.parse(buf[o:]);
+	if(err != nil)
+		error("parsing end of central directory: "+err);
+
+	if(eocdir.disknr != 0 || eocdir.diskcdirentries != eocdir.cdirentries)
+		error("split zip file, not supported");
+
+	b := bufio->fopen(fd, Bufio->OREAD);
+	if(b == nil)
+		error(sprint("fopen: %r"));
+	if(b.seek(eocdir.cdiroffset, Bufio->SEEKSTART) != eocdir.cdiroffset)
+		error(sprint("seek to central directory: %r"));
+	a := array[eocdir.cdirentries] of ref CDFhdr;
+	for(i := 0; i < len a; i++) {
+		(fhdr, ferr) := CDFhdr.read(b);
+		if(ferr != nil)
+			error("reading central directory file header: "+ferr);
+		a[i] = fhdr;
+	}
+
+	return (eocdir, a, nil);
+}
+
+findeocdir(buf: array of byte): (int, string)
+{
+	for(o := len buf-(4+2+2+2+2+4+4+2); o >= 0; o--)
+		if(buf[o] == byte 'P' && bufcmp(buf[o:o+4], eocentraldirsig) == 0)
+			return (o, nil);
+	return (-1, "cannot find end of central directory");
+}
+
+supported(f: ref Fhdr): string
+{
+	if((f.versneeded & 255) > Version)
+		return sprint("version too low for opening file, have %s, need %s", versstr(Version), versstr(f.versneeded & 255));
+	if(f.flags & Fcompressedpatched)
+		return "file is a patch, not supported";
+	if(f.flags & Fstrongcrypto)
+		return "file is new-style encrypted, not supported";
+	if(f.flags & Fencrypted)
+		return "file is encrypted, not supported";
+	return nil;
+}
+
+openfile(fd: ref Sys->FD, cdf: ref CDFhdr): (ref Sys->FD, ref Fhdr, string)
+{
+	(f, err) := Fhdr.read(fd, cdf.reloffset);
+	if(err != nil)
+		return (nil, nil, err);
+
+	err = supported(f);
+	if(err != nil)
+		return (nil, nil, err);
+
+	zfd: ref Sys->FD;
+	case f.comprmethod {
+	Mplain =>
+		zfd = pushbuf(fd, f.dataoff, int f.uncomprsize, cdf.crc32);
+	Mdeflate =>
+		# xxx +1 is a hack to prevent "premature end of stream" from our inflate.  is our inflate broken or is it info-zip 3.0?
+		zfd = pushfilter(fd, f.dataoff, 1+int f.comprsize, cdf.crc32);
+	* =>
+		return (nil, nil, sprint("compression method %q not supported", comprmethod(f.comprmethod)));
+	}
+	if(zfd == nil)
+		return (nil, nil, "opening file in zip failed");
+	return (zfd, f, nil);
+}
+
+readfhdr(fd: ref Sys->FD, cdf: ref CDFhdr): (ref Fhdr, string)
+{
+	(f, err) := Fhdr.read(fd, cdf.reloffset);
+	if(err == nil)
+		err = supported(f);
+	return (f, err);
+}
+
+# no crc32 protection, assumes supportedness has been checked already.
+pread(fd: ref Sys->FD, f: ref Fhdr, buf: array of byte, n: int, off: big): int
+{
+	if(f.comprmethod != Mplain) {
+		sys->werrstr("file is not plain (uncompressed)");
+		return -1;
+	}
+	if(off > f.uncomprsize)
+		off = f.uncomprsize;
+	if(off+big n > f.uncomprsize)
+		n = int (f.uncomprsize-off);
+	return sys->pread(fd, buf, n, f.dataoff+off);
+}
+
+cvtstr(d: array of byte, isutf8: int): string
+{
+	if(isutf8)
+		return string d;
+	return cp437->btos(Convcs->Startstate, d, -1).t1;
+}
+
+versstr(v: int): string
+{
+	v &= 255;
+	return sprint("%d.%d", v/10, v%10);
+}
+
+filegen: int;
+fileio(): (ref Sys->FD, ref Sys->FileIO)
+{
+	f := sprint("f%d", filegen++);
+	fio := sys->file2chan("/chan", f);
+	if(fio != nil)
+		rfd := sys->open("/chan/"+f, Sys->OREAD);
+	return (rfd, fio);
+}
+
+# as long as reads are sequential, keep track of the crc and verify at eof
+pushbuf0(fd: ref Sys->FD, off: big, n: int, fdc: chan of ref Sys->FD, hdrcrc: big)
+{
+	(rfd, fio) := fileio();
+	fdc <-= rfd;
+	if(rfd == nil)
+		return;
+
+	prevoff := 0;
+	docrc := 1;
+	crc := ~0;
+	end := off+big n;
+Fio:
+	for(;;) {
+		(roff0, count, nil, rc) := <-fio.read;
+		if(rc == nil)
+			return;
+		roff := off+big roff0;
+		rend := roff+big count;
+		if(roff < off)
+			roff = off;
+		if(rend > end)
+			rend = end;
+		nn := sys->pread(fd, buf := array[int (rend-roff)] of byte, len buf, roff);
+		if(nn < 0) {
+			rc <-= (nil, sprint("%r"));
+			continue;
+		}
+		docrc = docrc && prevoff == roff0;
+		if(docrc) {
+			crc = crc32(crc, buf[:nn]);
+			if(nn == 0 && ~crc != int hdrcrc) {
+				rc <-= (nil, sprint("crc mismatch, expected %bux, calculated %ux", hdrcrc, ~crc));
+				break Fio;
+			}
+			prevoff += nn;
+		}
+		rc <-= (buf[:nn], nil);
+	}
+}
+
+pushbuf(fd: ref Sys->FD, off: big, n: int, hdrcrc: big): ref Sys->FD
+{
+	spawn pushbuf0(fd, off, n, fdc := chan of ref Sys->FD, hdrcrc);
+	return <-fdc;
+}
+
+pushfilter0(fd: ref Sys->FD, off: big, n: int, fdc: chan of ref Sys->FD, hdrcrc: big)
+{
+	(rfd, fio) := fileio();
+	fdc <-= rfd;
+	if(rfd == nil)
+		return;
+
+	rqc := inflate->start("");
+	pid: int;
+	pick srq := <-rqc {
+	Start =>
+		pid = srq.pid;
+	* =>
+		return;
+	}
+
+	crc := ~0;
+	poff := 0;  # previous offset read
+	buf := array[0] of byte;
+Fio:
+	for(;;) {
+		(roff, count, nil, rc) := <-fio.read;
+		if(rc == nil)
+			break Fio;
+		if(roff != poff) {
+			rc <-= (nil, "random reads not allowed");
+			break Fio;
+		}
+
+	Filter:
+		while(len buf == 0)
+			pick rq := <-rqc {
+			Start =>
+				rc <-= (nil, "bogus start message from filter");
+				break Fio;
+			Fill =>
+				give := len rq.buf;
+				if(give > n)
+					give = n;
+				nn := sys->pread(fd, rq.buf, give, off);
+				rq.reply <-= nn;
+				if(nn < 0) {
+					rc <-= (nil, sprint("read: %r"));
+					break Fio;
+				}
+				off += big nn;
+				n -= nn;
+			Result =>
+				crc = crc32(crc, rq.buf);
+				buf = array[len rq.buf] of byte;
+				buf[:] = rq.buf;
+				rq.reply <-= 0;
+				break Filter;
+			Finished =>
+				if(len rq.buf != 0)
+					say(sprint("%d leftover bytes", len rq.buf));
+				crc = ~crc;
+				if(crc != int hdrcrc) {
+					rc <-= (nil, sprint("crc mismatch, expected %bux, calculated %ux", hdrcrc, crc));
+					break Fio;
+				}
+				rc <-= (array[0] of byte, nil);
+				break Fio;
+			Info =>
+				say("inflate: "+rq.msg);
+			Error =>
+				rc <-= (nil, rq.e);
+				break Fio;
+			}
+
+		give := count;
+		if(give > len buf)
+			give = len buf;
+		r := buf[:give];
+		buf = buf[give:];
+		poff += give;
+		rc <-= (r, nil);
+	}
+	kill(pid);
+}
+
+pushfilter(fd: ref Sys->FD, off: big, n: int, hdrcrc: big): ref Sys->FD
+{
+	spawn pushfilter0(fd, off, n, fdc := chan of ref Sys->FD, hdrcrc);
+	return <-fdc;
+}
+
+
+bufcmp(a, b: array of byte): int
+{
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return int a[i]-int b[i];
+	return 0;
+}
+
+g16(d: array of byte, o: int): (int, int)
+{
+	if(o+2 > len d)
+		raise "get:short buffer";
+	v := 0;
+	v |= int d[o++]<<0;
+	v |= int d[o++]<<8;
+	return (v, o);
+}
+
+g32(d: array of byte, o: int): (big, int)
+{
+	if(o+2 > len d)
+		raise "get:short buffer";
+	v := big 0;
+	v |= big d[o++]<<0;
+	v |= big d[o++]<<8;
+	v |= big d[o++]<<16;
+	v |= big d[o++]<<24;
+	return (v, o);
+}
+
+gstr(d: array of byte, o: int): (array of byte, int)
+{
+	n: int;
+	(n, o) = g16(d, o);
+	if(o+n > len d)
+		raise "get:short buffer for string";
+	buf := array[n] of byte;
+	buf[:] = d[o:o+n];
+	return (buf, o+n);
+}
+
+gbuf(n: int, d: array of byte, o: int): (array of byte, int)
+{
+	if(o+n > len d)
+		raise "get:short buffer for buffer";
+	buf := array[n] of byte;
+	buf[:] = d[o:o+n];
+	return (buf, o+n);
+}
+
+gbufstr(isutf8: int, n: int, d: array of byte, o: int): (string, int)
+{
+	buf: array of byte;
+	(buf, o) = gbuf(n, d, o);
+	return (cvtstr(buf, isutf8), o);
+}
+
+p16(d: array of byte, o: int, v: int): int
+{
+	d[o++] = byte (v>>0);
+	d[o++] = byte (v>>8);
+	return o;
+}
+
+p32(d: array of byte, o: int, v: big): int
+{
+	d[o++] = byte (v>>0);
+	d[o++] = byte (v>>8);
+	d[o++] = byte (v>>16);
+	d[o++] = byte (v>>24);
+	return o;
+}
+
+pbuf(d: array of byte, o: int, buf: array of byte): int
+{
+	d[o:] = buf;
+	return o+len buf;
+}
+
+preadn(fd: ref Sys->FD, buf: array of byte, n: int, off: big): int 
+{
+	have := 0;
+	for(;;) {
+		nn := sys->pread(fd, buf[have:], n, off);
+		if(nn < 0)
+			return nn;
+		if(nn == 0)
+			break;
+		have += nn;
+		off += big nn;
+		n -= nn;
+	}
+	return have;
+}
+
+breadn(b: ref Iobuf, buf: array of byte, n: int): int
+{
+	have := 0;
+	for(;;) {
+		nn := b.read(buf[have:], n-have);
+		if(nn < 0)
+			return nn;
+		if(nn == 0)
+			break;
+		have += nn;
+	}
+	return have;
+}
+
+
+mkcrcval(poly, c: int): int
+{
+	for(j := 0; j < 8; j++)
+		if(c & 1)
+			c = poly ^ ((c>>1) & 16r7fffffff);
+		else
+			c = (c>>1) & 16r7fffffff;
+	return c;
+}
+
+mkcrctab(poly: int): array of int
+{
+	tab := array[256] of int;
+        for(i := 0; i < 256; i++)
+		tab[i] = mkcrcval(poly, i);
+	return tab;
+}
+
+crc32(crc: int, buf: array of byte): int
+{
+	n := len buf;
+	for(i := 0; i < n; i++)
+                crc = crc32tab[(crc ^ int buf[i]) & 255] ^ ((crc>>8) & 16rffffff);
+	return crc;
+}
+
+
+rev[T](l: list of T): list of T
+{
+	r: list of T;
+	for(; l != nil; l = tl l)
+		r = hd l::r;
+	return r;
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open(sprint("/prog/%d/ctl", pid), Sys->OWRITE), "kill");
+}
+
+
+say(s: string)
+{
+	if(dflag)
+		sys->fprint(sys->fildes(2), "zip: %s\n", s);
+}
--- a/lib/emptydirs
+++ b/lib/emptydirs
@@ -186,3 +186,4 @@
 dis/usb
 dis/wm
 dis/wm/brutus
+dis/zip
--- /dev/null
+++ b/man/1/getzip
@@ -1,0 +1,104 @@
+.TH GETZIP 1
+.SH NAME
+getzip, lszip, putzip \- zip file utilities
+.SH SYNOPSIS
+.B zip/getzip
+[
+.B -dv
+] [
+.B -k
+]
+.I zipfile
+[
+.I path
+.I ...
+]
+.br
+.B zip/lszip
+[
+.B -d
+]
+.I zipfile
+.br
+.B zip/putzip
+[
+.I -dvp
+]
+.I zipfile
+[
+.I path
+.I ...
+]
+.SH DESCRIPTION
+.BR Getzip ,
+.B lszip
+and
+.B putzip
+read, list and create zip files.
+.I Zipfile
+is the file to be read or created.
+.PP
+.B Getzip
+extracts only the
+.I paths
+given on the command-line.  If no paths are specified, all files in the zip file are extracted.
+.PP
+.B Lszip
+lists the files present in the zip file.
+.PP
+.B Putzip
+creates a zip file.
+If
+.I paths
+are given on the command-line, those paths and their children are added to the zip file.
+Otherwise, a list of paths to put in the zip file are read from
+standard input.  Directories from the standard input are not added
+recursively.
+.TP
+
+.PP
+Options
+.PP
+.TP
+.B -d
+Print debugging information.  A second
+.B -d prints more information.
+.TP
+.B -v
+Be verbose.
+For
+.B getzip
+this prints the files extracted.
+For
+.B putzip
+this prints the files added to the zip file.
+.TP
+.B -k
+Keep existing files.  For
+.B getzip
+only.
+By default, existing files are overwritten.
+.TP
+.B -p
+Do not compress files added to the zip file.  For
+.B putzip
+only.
+
+.SH SOURCE
+.B /appl/cmd/zip/getzip.b
+.br
+.B /appl/cmd/zip/lszip.b
+.br
+.B /appl/cmd/zip/putzip.b
+.br
+.B /appl/lib/zip.b
+.br
+.B /appl/lib/zip.m
+.SH SEE ALSO
+.IR gettar (1),
+.IR zipstream (1),
+.IR zipfs (4)
+.SH BUGS
+Zip64 extensions are not supported.
+.br
+Encrypted zip files are not supported.
--- /dev/null
+++ b/man/1/zipstream
@@ -1,0 +1,39 @@
+.TH ZIPSTREAM 1
+.SH NAME
+zipstream \- rewrite zip file for streaming
+.SH SYNOPSIS
+.B zip/zipstream
+[
+.B -d
+] [
+.B -o
+]
+.I zipfile
+.SH DESCRIPTION
+.B Zipstream
+writes
+.I zipfile
+to standard output, placing the central directory (listing all files and their details) at the start of the file instead of at the end.
+.PP
+If
+.B -o
+is specified, a list of files is read from standard input (separated by newline) and those files moved to the start of the new zip file.
+Files must be specified at most once.
+Files not specified will remain in the same order, but after the files that were specified.
+.PP
+Option
+.B -d
+increases debugging output.
+.SH SOURCE
+.B /appl/cmd/zip/zipstream.b
+.br
+.B /appl/lib/zip.b
+.br
+.B /appl/lib/zip.m
+.SH SEE ALSO
+.IR getzip (1),
+.IR zipfs (4)
+.SH BUGS
+Not all zip utilities can read zip files generated by zipstream.  Most notably Info-ZIP and 7z.
+.PP
+The ``end of the central directory'' (pointing to the actual central directory at the beginning) is still at the end of the file.
--- /dev/null
+++ b/man/4/zipfs
@@ -1,0 +1,47 @@
+.TH ZIPFS 4
+.SH NAME
+zipfs \- mount zip archive
+.SH SYNOPSIS
+mount {
+.B zip/zipfs
+[
+.B -dDp
+]
+.I zipfile
+}
+mtpt
+.SH DESCRIPTION
+.I Zipfs
+makes the contents of
+.I zipfile
+available in the file system, for reading.
+.I Zipfs
+serves the styx protocol on its file descriptor 0.
+.PP
+Options
+.TP
+.B -d
+Print debugging information.  A second
+.B -d
+increases output.
+.TP
+.B -D
+Print a trace of styx messages.
+.TP
+.B -p
+Read uncompressed files in the zip file more efficiently.  The downside is that checksums are not checked.
+.SH SOURCE
+.B /appl/cmd/zip/zipfs.b
+.br
+.B /appl/lib/zip.b
+.br
+.B /module/zip.m
+.SH SEE ALSO
+.IR getzip (1),
+.IR zipstream (1).
+.SH BUGS
+Zip64 extensions are not supported.
+.br
+Encrypted archives are not supported.
+.br
+Only uncompressed and deflate-compressed files are supported.
--- /dev/null
+++ b/module/zip.m
@@ -1,0 +1,113 @@
+Zip: module
+{
+	PATH:	con "/dis/lib/zip.dis";
+
+	dflag:	int;
+	init:	fn(): string;
+
+	# compression methods.  only plain & deflate supported.
+	Mplain,
+	Mshrunk,
+	Mreduced1, Mreduced2, Mreduced3, Mreduced4,
+	Mimplode, Mtokenize,
+	Mdeflate, Mdeflate64,
+	Mpkwareimplode,
+	Mreserved0,
+	Mbzip2,
+	Mreserved1,
+	Mlzma:		con iota+0;
+	Mibmterse,
+	Mlz77z:		con iota+18;
+	Mwavpack,
+	Mppmdi1:	con iota+97;
+
+	# general purpose flags
+	Fencrypted:		con 1<<0;
+	Flocaldatadescr:	con 1<<3;  # crc & sizes in fhdr are 0, use "data descriptor" following fhdr
+	Fcompressedpatched:	con 1<<5;
+	Fstrongcrypto:		con 1<<6;
+	Futf8:			con 1<<11;
+	Fcdirencrypted:		con 1<<13;
+
+	# internal file attributes
+	IFArecord:	con 1<<1;
+
+	Version: con 20;	# supported for reading
+
+	Extra: adt {
+		l:	list of ref (int, array of byte);
+
+		parse:	fn(d: array of byte): (ref Extra, string);
+		pack:	fn(e: self ref Extra): array of byte;
+		text:	fn(e: self ref Extra): string;
+	};
+
+	Fhdr: adt {
+		versneeded:	int;
+		flags:		int;
+		comprmethod:	int;
+		filemtime:	int;
+		filemdate:	int;
+		mtime:		int;  # not in file, unix epoch mtime based on filemtime & filemdate
+		crc32:		big;
+		comprsize:	big;
+		uncomprsize:	big;
+		filename:	string;
+		extra:		ref Extra;
+		dataoff:	big;  # not in file
+
+		mk:	fn(f: ref CDFhdr): ref Fhdr;
+		parse:	fn(buf: array of byte, off: big): (ref Fhdr, string);
+		read:	fn(fd: ref Sys->FD, off: big): (ref Fhdr, string);
+		pack:	fn(f: self ref Fhdr): array of byte;
+		text:	fn(f: self ref Fhdr): string;
+	};
+
+	CDFhdr: adt {
+		versmadeby:	int;
+		versneeded:	int;
+		flags:		int;
+		comprmethod:	int;
+		filemtime:	int;
+		filemdate:	int;
+		mtime:		int;  # not in file, unix epoch mtime based on filemtime & filemdate
+		crc32:		big;
+		comprsize:	big;
+		uncomprsize:	big;
+		filename:	string;
+		extra:		ref Extra;
+		comment:	string;
+		disknrstart:	int;
+		intattr:	int;
+		extattr:	big;
+		reloffset:	big;
+
+		mk:	fn(f: ref Fhdr, off: big): ref CDFhdr;
+		parse:	fn(buf: array of byte): (ref CDFhdr, string);
+		read:	fn(b: ref Bufio->Iobuf): (ref CDFhdr, string);
+		pack:	fn(f: self ref CDFhdr): array of byte;
+		text:	fn(f: self ref CDFhdr): string;
+	};
+
+	Endofcdir: adt {
+		disknr:		int;
+		diskcdir:	int;
+		diskcdirentries:	int;
+		cdirentries:	int;
+		cdirsize:	big;
+		cdiroffset:	big;
+		comment:	array of byte;
+
+		parse:	fn(buf: array of byte): (ref Endofcdir, string);
+		pack:	fn(e: self ref Endofcdir): array of byte;
+		text:	fn(e: self ref Endofcdir): string;
+	};
+
+	comprmethod:	fn(m: int): string;
+	open:		fn(fd: ref Sys->FD): (ref Endofcdir, array of ref CDFhdr, string);
+	openfile:	fn(fd: ref Sys->FD, f: ref CDFhdr): (ref Sys->FD, ref Fhdr, string);
+	readfhdr:	fn(fd: ref Sys->FD, f: ref CDFhdr): (ref Fhdr, string);
+	pread:		fn(fd: ref Sys->FD, f: ref Fhdr, buf: array of byte, n: int, off: big): int;
+	sanitizepath:	fn(s: string): string;
+	crc32:		fn(crc: int, buf: array of byte): int;
+};