code: purgatorio

ref: 60ecd07e6d3f5786c8723dc9172c35d580fdadc8
dir: /appl/cmd/disk/kfs.b/

View raw version
implement Kfs;

#
# Copyright © 1991-2003 Lucent Technologies Inc.
# Limbo version Copyright © 2004 Vita Nuova Holdings Limited
#

#
# TO DO:
#	- sync proc; Bmod; process structure
#	- swiz?

include "sys.m";
	sys: Sys;
	Qid, Dir: import Sys;
	DMEXCL, DMAPPEND, DMDIR: import Sys;
	QTEXCL, QTAPPEND, QTDIR: import Sys;

include "draw.m";

include "styx.m";
	styx: Styx;
	Tmsg, Rmsg: import styx;
	NOFID, OEXEC, ORCLOSE, OREAD, OWRITE, ORDWR, OTRUNC: import Styx;
	IOHDRSZ: import Styx;

include "daytime.m";
	daytime: Daytime;
	now: import daytime;

include "arg.m";

Kfs: module
{
	init:	fn(nil: ref Draw->Context, nil: list of string);
};

MAXBUFSIZE:	con 16*1024;

#
#  fundamental constants
#
NAMELEN: con 28;	# size of names, including null byte
NDBLOCK:	con 6;	# number of direct blocks in Dentry
MAXFILESIZE:	con big 16r7FFFFFFF;	# Plan 9's limit (kfs's size is signed)

SUPERADDR: con 1;
ROOTADDR: con 2;

QPDIR:	con int (1<<31);
QPNONE: con 0;
QPROOT: con 1;
QPSUPER: con 2;

#
# don't change, these are the mode bits on disc
#
DALLOC: con 16r8000;
DDIR:	con 16r4000;
DAPND:	con 16r2000;
DLOCK:	con 16r1000;
DREAD:	con 4;
DWRITE:	con 2;
DEXEC:	con 1;

#
# other constants
#

MINUTE:	con 60;
TLOCK:	con 5*MINUTE;
NTLOCK:	con 200;	# number of active file locks

Buffering: con 1;

FID1, FID2, FID3: con 1+iota;

None: con 0;	# user ID for "none"
Noworld: con 9999;	# conventional id for "noworld" group

Lock: adt
{
	c: chan of int;
	new:	fn(): ref Lock;
	lock:	fn(c: self ref Lock);
	canlock:	fn(c: self ref Lock): int;
	unlock:	fn(c: self ref Lock);
};

Dentry: adt
{
	name:	string;
	uid:	int;
	gid:	int;
	muid:	int;	# not set by plan 9's kfs
	mode:	int;	# mode bits on disc: DALLOC etc
	qid:	Qid;	# 9p1 format on disc
	size:	big;	# only 32-bits on disc, and Plan 9 limits it to signed
	atime:	int;
	mtime:	int;

	iob:	ref Iobuf;	# locked block containing directory entry, when in memory
	buf:	array of byte;	# pointer into block to packed directory entry, when in memory
	mod:	int;	# bits of buf that need updating

	unpack:	fn(a: array of byte): ref Dentry;
	get:	fn(p: ref Iobuf, slot: int): ref Dentry;
	geta:	fn(d: ref Device, addr: int, slot: int, qpath: int, mode: int): (ref Dentry, string);
	getd:	fn(f: ref File, mode: int): (ref Dentry, string);
	put:	fn(d: self ref Dentry);
	access:	fn(d: self ref Dentry, f: int, uid: int);
	change:	fn(d: self ref Dentry, f: int);
	release:	fn(d: self ref Dentry);
	getblk:	fn(d: self ref Dentry, a: int, tag: int): ref Iobuf;
	getblk1:	fn(d: self ref Dentry, a: int, tag: int): ref Iobuf;
	rel2abs:	fn(d: self ref Dentry, a: int, tag: int, putb: int): int;
	trunc:	fn(d: self ref Dentry, uid: int);
	update:	fn(d: self ref Dentry);
	print:	fn(d: self ref Dentry);
};

Uname, Uids, Umode, Uqid, Usize, Utime: con 1<<iota;	# Dentry.mod

#
# disc structure:
#	Tag:	pad[2] tag[2] path[4]
Tagsize: con 2+2+4;

Tag: adt
{
	tag:	int;
	path:	int;

	unpack:	fn(a: array of byte): Tag;
	pack:	fn(t: self Tag, a: array of byte);
};

Superb: adt
{
	iob:	ref Iobuf;

	fstart:	int;
	fsize:	int;
	tfree:	int;
	qidgen:	int;		# generator for unique ids

	fsok:	int;

	fbuf:	array of byte;	# nfree[4] free[FEPERBLK*4]; aliased into containing block

	get:	fn(dev: ref Device, flags: int): ref Superb;
	touched:	fn(s: self ref Superb);
	put:	fn(s: self ref Superb);
	print:	fn(s: self ref Superb);

	pack:	fn(s: self ref Superb, a: array of byte);
	unpack:	fn(a: array of byte): ref Superb;
};

Device: adt
{
	fd:	ref Sys->FD;
	ronly:	int;
	# could put locks here if necessary
	# partitioning by ds(3)
};

#
# one for each locked qid
#
Tlock: adt
{
	dev:	ref Device;
	time:	int;
	qpath:	int;
	file:	cyclic ref File;	# TO DO: probably not needed
};

File: adt
{
	qlock:	chan of int;
	qid:	Qid;
	wpath:	ref Wpath;
	tlock:	cyclic ref Tlock;		# if file is locked
	fs:	ref Device;
	addr:	int;
	slot:	int;
	lastra:	int;		# read ahead address
	fid:	int;
	uid:	int;
	open:	int;
	cons:	int;	# if opened by console
	doffset: big;	# directory reading
	dvers:	int;
	dslot:	int;

	new:	fn(fid: int): ref File;
	access:	fn(f: self ref File, d: ref Dentry, mode: int): int;
	lock:	fn(f: self ref File);
	unlock:	fn(f: self ref File);
};

FREAD, FWRITE, FREMOV, FWSTAT: con 1<<iota;	# File.open

Chan: adt
{
	fd:	ref Sys->FD;			# fd request came in on
#	rlock, wlock: QLock;		# lock for reading/writing messages on cp
	flags:	int;
	flist:	list of ref File;			# active files
	fqlock:	chan of int;
#	reflock:	RWLock;		# lock for Tflush
	msize:	int;			# version

	new:	fn(fd: ref Sys->FD): ref Chan;
	getfid:	fn(c: self ref Chan, fid: int, flag: int): ref File;
	putfid:	fn(c: self ref Chan, f: ref File);
	flock: fn(nil: self ref Chan);
	funlock:	fn(nil: self ref Chan);
};

Hiob: adt
{
	link:	ref Iobuf;	# TO DO: eliminate circular list
	lk:	ref Lock;
	niob: int;

	newbuf:	fn(h: self ref Hiob): ref Iobuf;
};

Iobuf: adt
{
	qlock:	chan of int;
	dev:	ref Device;
	fore:	cyclic ref Iobuf;		# lru hash chain
	back:	cyclic ref Iobuf;		# for lru
	iobuf:	array of byte;		# only active while locked
	xiobuf:	array of byte;	# "real" buffer pointer
	addr:	int;
	flags:	int;

	get:	fn(dev: ref Device, addr: int, flags: int):ref Iobuf;
	put:	fn(iob: self ref Iobuf);
	lock:	fn(iob: self ref Iobuf);
	canlock:	fn(iob: self ref Iobuf): int;
	unlock:	fn(iob: self ref Iobuf);

	checktag:	fn(iob: self ref Iobuf, tag: int, qpath: int): int;
	settag:	fn(iob: self ref Iobuf, tag: int, qpath: int);
};

Wpath: adt
{
	up: cyclic ref Wpath;		# pointer upwards in path
	addr: int;		# directory entry addr
	slot: int;		# directory entry slot
};

#
#  error codes generated from the file server
#
Eaccess: con "access permission denied";
Ealloc: con "phase error -- directory entry not allocated";
Eauth: con "authentication failed";
Eauthmsg: con "kfs: authentication not required";
Ebadspc: con "attach -- bad specifier";
Ebadu: con "attach -- privileged user";
Ebroken: con "close/read/write -- lock is broken";
Echar: con "bad character in directory name";
Econvert: con "protocol botch";
Ecount: con "read/write -- count too big";
Edir1: con "walk -- in a non-directory";
Edir2: con "create -- in a non-directory";
Edot: con "create -- . and .. illegal names";
Eempty: con "remove -- directory not empty";
Eentry: con "directory entry not found";
Eexist: con "create -- file exists";
Efid: con "unknown fid";
Efidinuse: con "fid already in use";
Efull: con "file system full";
Elocked: con "open/create -- file is locked";
Emode: con "open/create -- unknown mode";
Ename: con "create/wstat -- bad character in file name";
Enotd: con "wstat -- attempt to change directory";
Enotg: con "wstat -- not in group";
Enotl: con "wstat -- attempt to change length";
Enotm: con "wstat -- unknown type/mode";
Enotu: con "wstat -- not owner";
Eoffset: con "read/write -- offset negative";
Eopen: con "read/write -- on non open fid";
Ephase: con "phase error -- cannot happen";
Eqid: con "phase error -- qid does not match";
Eqidmode: con "wstat -- qid.qtype/dir.mode mismatch";
Eronly: con "file system read only";
Ersc: con "it's russ's fault.  bug him.";
Esystem: con "kfs system error";
Etoolong: con "name too long";
Etoobig: con "write -- file size limit";
Ewalk: con "walk -- too many (system wide)";

#
#  tags on block
#
Tnone,
Tsuper,			# the super block
Tdir,			# directory contents
Tind1,			# points to blocks
Tind2,			# points to Tind1
Tfile,			# file contents
Tfree,			# in free list
Tbuck,			# cache fs bucket
Tvirgo,			# fake worm virgin bits
Tcache,			# cw cache things
MAXTAG: con iota;

#
#  flags to Iobuf.get
#
	Bread,	# read the block if miss
	Bprobe,	# return null if miss
	Bmod,	# set modified bit in buffer
	Bimm,	# set immediate bit in buffer
	Bres:		# never renamed
	con 1<<iota;

#
#  check flags
#
	Crdall,	# read all files
	Ctag,	# rebuild tags
	Cpfile,	# print files
	Cpdir,	# print directories
	Cfree,	# rebuild free list
	Cream,	# clear all bad tags
	Cbad,	# clear all bad blocks
	Ctouch,	# touch old dir and indir
	Cquiet:	# report just nasty things
	con 1<<iota;

#
#  buffer size variables, determined by RBUFSIZE
#
RBUFSIZE: int;
BUFSIZE: int;
DIRPERBUF: int;
INDPERBUF: int;
INDPERBUF2: int;
FEPERBUF: int;

emptyblock: array of byte;

wrenfd: ref Sys->FD;
thedevice: ref Device;
devnone: ref Device;
wstatallow := 0;
writeallow := 0;
writegroup := 0;

ream := 0;
readonly := 0;
noatime := 0;
localfs: con 1;
conschan: ref Chan;
consuid := -1;
consgid := -1;
debug := 0;
kfsname: string;
consoleout: chan of string;
mainlock: ref Lock;
pids: list of int;

noqid: Qid;

init(nil: ref Draw->Context, args: list of string)
{
	sys = load Sys Sys->PATH;
	styx = load Styx Styx->PATH;
	daytime = load Daytime Daytime->PATH;

	styx->init();


	arg := load Arg Arg->PATH;
	if(arg == nil)
		error(sys->sprint("can't load %s: %r", Arg->PATH));
	arg->init(args);
	arg->setusage("disk/kfs [-r [-b bufsize]] [-cADPRW] [-n name] kfsfile");
	bufsize := 1024;
	nocheck := 0;
	while((o := arg->opt()) != 0)
		case o {
		'c' => nocheck = 1;
		'r' =>	ream = 1;
		'b' => bufsize = int arg->earg();
		'D' => debug = !debug;
		'P' => writeallow = 1;
		'W' => wstatallow = 1;
		'R' => readonly = 1;
		'A' => noatime = 1;	# mainly useful for flash
		'n' => kfsname = arg->earg();
		* =>	arg->usage();
		}
	args = arg->argv();
	if(args == nil)
		arg->usage();
	arg = nil;

	devnone = ref Device(nil, 1);
	mainlock = Lock.new();

	conschan = Chan.new(nil);
	conschan.msize = Styx->MAXRPC;

	mode := Sys->ORDWR;
	if(readonly)
		mode = Sys->OREAD;
	wrenfd = sys->open(hd args, mode);
	if(wrenfd == nil)
		error(sys->sprint("can't open %s: %r", hd args));
	thedevice = ref Device(wrenfd, readonly);
	if(ream){
		if(bufsize <= 0 || bufsize % 512 || bufsize > MAXBUFSIZE)
			error(sys->sprint("invalid block size %d", bufsize));
		RBUFSIZE = bufsize;
		wrenream(thedevice);
	}else{
		if(!wreninit(thedevice))
			error("kfs magic in trouble");
	}
	BUFSIZE = RBUFSIZE - Tagsize;
	DIRPERBUF = BUFSIZE / Dentrysize;
	INDPERBUF = BUFSIZE / 4;
	INDPERBUF2 = INDPERBUF * INDPERBUF;
	FEPERBUF = (BUFSIZE - Super1size - 4) / 4;
	emptyblock = array[RBUFSIZE] of {* => byte 0};

	iobufinit(30);

	if(ream){
		superream(thedevice, SUPERADDR);
		rootream(thedevice, ROOTADDR);
		wstatallow = writeallow = 1;
	}
	if(wrencheck(wrenfd))
		error("kfs super/root in trouble");

	if(!ream && !readonly && !superok(0)){
		sys->print("kfs needs check\n");
		if(!nocheck)
			check(thedevice, Cquiet|Cfree);
	}

	(d, e) := Dentry.geta(thedevice, ROOTADDR, 0, QPROOT, Bread);
	if(d != nil && !(d.mode & DDIR))
		e = "not a directory";
	if(e != nil)
		error("bad root: "+e);
	if(debug)
		d.print();
	d.put();

	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);

	sys->pctl(Sys->NEWFD, wrenfd.fd :: 0 :: 1 :: 2 :: nil);
	wrenfd = sys->fildes(wrenfd.fd);
	thedevice.fd = wrenfd;

	c := chan of int;

	if(Buffering){
		spawn syncproc(c);
		pid := <-c;
		if(pid)
			pids = pid :: pids;
	}
	spawn consinit(c);
	pid := <- c;
	if(pid)
		pids = pid :: pids;

	spawn kfs(sys->fildes(0));
}

error(s: string)
{
	sys->fprint(sys->fildes(2), "kfs: %s\n", s);
	for(; pids != nil; pids = tl pids)
		kill(hd pids);
	raise "fail:error";
}

panic(s: string)
{
	sys->fprint(sys->fildes(2), "kfs: panic: %s\n", s);
	for(; pids != nil; pids = tl pids)
		kill(hd pids);
	raise "panic";
}

syncproc(c: chan of int)
{
	c <-= 0;
}

shutdown()
{
	for(; pids != nil; pids = tl pids)
		kill(hd pids);
	# TO DO: when Bmod deferred, must sync
	# sync super block
	if(!readonly && superok(1)){
		# ;
	}
	iobufclear();
}

kill(pid: int)
{
	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
	if(fd != nil)
		sys->fprint(fd, "kill");
}

#
# limited file system support for console
#
kattach(fid: int): string
{
	return applycons(ref Tmsg.Attach(1, fid, NOFID, "adm", "")).t1;
}

kopen(oldfid: int, newfid: int, names: array of string, mode: int): string
{
	(r1, e1) := applycons(ref Tmsg.Walk(1, oldfid, newfid, names));
	if(r1 != nil){
		pick m := r1 {
		Walk =>
			if(len m.qids != len names){
				kclose(newfid);
				cprint(Eexist);
				return Eexist;
			}
		* =>
			return "unexpected reply";
		}
		(r1, e1) = applycons(ref Tmsg.Open(1, newfid, mode));
		if(e1 != nil){
			kclose(newfid);
			cprint(sys->sprint("open: %s", e1));
		}
	}
	return e1;
}

kread(fid: int, offset: int, nbytes: int): (array of byte, string)
{
	(r, e) := applycons(ref Tmsg.Read(1, fid, big offset, nbytes));
	if(r != nil){
		pick m := r {
		Read =>
			return (m.data, nil);
		* =>
			return (nil, "unexpected reply");
		}
	}
	cprint(sys->sprint("read error: %s", e));
	return (nil, e);
}

kclose(fid: int)
{
	applycons(ref Tmsg.Clunk(1, fid));
}

applycons(t: ref Tmsg): (ref Rmsg, string)
{
	r := apply(conschan, t);
	pick m := r {
	Error =>
		if(debug)
			cprint(sys->sprint("%s: %s\n", t.text(), m.ename));
		return (nil, m.ename);
	}
	return (r, nil);
}

#
# always reads /adm/users in userinit(), then
# optionally serves the command file, if used.
#
Req: adt {
	nbytes:	int;
	rc:	chan of (array of byte, string);
};

consinit(c: chan of int)
{
	kattach(FID1);
	userinit();
	if(kfsname == nil){
		c <-= 0;
		exit;
	}
	cfname := "kfs."+kfsname+".cmd";
	sys->bind("#s", "/chan", Sys->MBEFORE);
	file := sys->file2chan("/chan", cfname);
	if(file == nil)
		error(sys->sprint("can't create /chan/%s: %r", cfname));
	c <-= sys->pctl(0, nil);
	consc := chan of string;
	checkend := chan of int;
	cdata: array of byte;
	pending: ref Req;
	cfid := -1;
	for(;;) alt{
	(nil, nbytes, fid, rc) := <-file.read =>
		if(rc == nil)
			break;
		if(cfid == -1)
			cfid = fid;
		if(fid != cfid || pending != nil){
			rc <-= (nil, "kfs.cmd is busy");
			break;
		}
		if(cdata != nil){
			cdata = reply(rc, nbytes, cdata);
			break;
		}
		if(nbytes <= 0 || consoleout == nil){
			rc <-= (nil, nil);
			break;
		}
		pending = ref Req(nbytes, rc);
		consc = consoleout;
	(nil, data, fid, wc) := <-file.write =>
		if(cfid == -1)
			cfid = fid;
		if(wc == nil){
			if(fid == cfid){
				cfid = -1;
				pending = nil;
				cdata = nil;	# discard unread data from last command
				if((consc = consoleout) == nil)
					consc = chan of string;
			}
			break;
		}
		if(fid != cfid){
			wc <-= (0, "kfs.cmd is busy");
			break;
		}
		(nf, fld) := sys->tokenize(string data, " \t\n\r");
		if(nf < 1){
			wc <-= (0, "illegal kfs request");
			break;
		}
		case hd fld {
		"check" =>
			if(consoleout != nil){
				wc <-= (0, "check in progress");
				break;
			}
			f := 0;
			if(nf > 1){
				f = checkflags(hd tl fld);
				if(f < 0){
					wc <-= (0, "illegal check flag: "+hd tl fld);
					break;
				}
			}
			consoleout = chan of string;
			spawn checkproc(checkend, f);
			wc <-= (len data, nil);
			consc = consoleout;
		"users" or "user" =>
			cmd_users();
			wc <-= (len data, nil);
		"sync" =>
			# nothing TO DO until writes are buffered
			wc <-= (len data, nil);
		"allow" =>
			wstatallow = writeallow = 1;
			wc <-= (len data, nil);
		"allowoff" or "disallow" =>
			wstatallow = writeallow = 0;
			wc <-= (len data, nil);
		* =>
			wc <-= (0, "unknown kfs request");
			continue;
		}
	<-checkend =>
		consoleout = nil;
		consc = chan of string;
	s := <-consc =>
		#sys->print("<-%s\n", s);
		req := pending;
		pending = nil;
		if(req != nil)
			cdata = reply(req.rc, req.nbytes, array of byte s);
		else
			cdata = array of byte s;
		if(cdata != nil && cfid != -1)
			consc = chan of string;
	}
}

reply(rc: chan of (array of byte, string), nbytes: int, a: array of byte): array of byte
{
	if(len a < nbytes)
		nbytes = len a;
	rc <-= (a[0:nbytes], nil);
	if(nbytes == len a)
		return nil;
	return a[nbytes:];
}

checkproc(c: chan of int, flags: int)
{
	mainlock.lock();
	check(thedevice, flags);
	mainlock.unlock();
	c <-= 1;
}

#
# normal kfs service
#
kfs(rfd: ref Sys->FD)
{
	cp := Chan.new(rfd);
	while((t := Tmsg.read(rfd, cp.msize)) != nil){
		if(debug)
			sys->print("<- %s\n", t.text());
		r := apply(cp, t);
		pick m := r {
		Error =>
			r.tag = t.tag;
		}
		if(debug)
			sys->print("-> %s\n", r.text());
		rbuf := r.pack();
		if(rbuf == nil)
			panic("Rmsg.pack");
		if(sys->write(rfd, rbuf, len rbuf) != len rbuf)
			panic("mount write");
	}
	shutdown();
}

apply(cp: ref Chan, t: ref Tmsg): ref Rmsg
{
	mainlock.lock();	# TO DO: this is just to keep console and kfs from colliding
	r: ref Rmsg;
	pick m := t {
	Readerror =>
		error(sys->sprint("mount read error: %s", m.error));
	Version =>
		r = rversion(cp, m);
	Auth =>
		r = rauth(cp, m);
	Flush =>
		r = rflush(cp, m);
	Attach =>
		r = rattach(cp, m);
	Walk =>
		r = rwalk(cp, m);
	Open =>
		r = ropen(cp, m);
	Create =>
		r = rcreate(cp, m);
	Read =>
		r = rread(cp, m);
	Write =>
		r = rwrite(cp, m);
	Clunk =>
		r = rclunk(cp, m);
	Remove =>
		r = rremove(cp, m);
	Stat =>
		r = rstat(cp, m);
	Wstat =>
		r = rwstat(cp, m);
	* =>
		panic("Styx mtype");
		return nil;
	}
	mainlock.unlock();
	return r;
}

rversion(cp: ref Chan, t: ref Tmsg.Version): ref Rmsg
{
	cp.msize = RBUFSIZE+IOHDRSZ;
	if(cp.msize < Styx->MAXRPC)
		cp.msize = Styx->MAXRPC;
	(msize, version) := styx->compatible(t, Styx->MAXRPC, Styx->VERSION);
	if(msize < 256)
		return ref Rmsg.Error(t.tag, "message size too small");
	return ref Rmsg.Version(t.tag, msize, version);
}

rauth(nil: ref Chan, t: ref Tmsg.Auth): ref Rmsg
{
	return ref Rmsg.Error(t.tag, Eauthmsg);
}

rflush(nil: ref Chan, t: ref Tmsg.Flush): ref Rmsg
{
	# runlock(cp.reflock);
	# wlock(cp.reflock);
	# wunlock(cp.reflock);
	# rlock(cp.reflock);
	return ref Rmsg.Flush(t.tag);
}

err(t: ref Tmsg, s: string): ref Rmsg.Error
{
	return ref Rmsg.Error(t.tag, s);
}

ferr(t: ref Tmsg, s: string, file: ref File, p: ref Iobuf): ref Rmsg.Error
{
	if(p != nil)
		p.put();
	if(file != nil)
		file.unlock();
	return ref Rmsg.Error(t.tag, s);
}

File.new(fid: int): ref File
{
	f := ref File;
	f.qlock = chan[1] of int;
	f.fid = fid;
	f.cons = 0;
	f.tlock = nil;
	f.wpath = nil;
	f.doffset = big 0;
	f.dvers = 0;
	f.dslot = 0;
	f.uid = None;
	f.cons = 0;
#	f.cuid = None;
	return f;
}

#
# returns a locked file structure
#

Chan.getfid(cp: self ref Chan, fid: int, flag: int): ref File
{
	if(fid == NOFID)
		return nil;
	cp.flock();
	for(l := cp.flist; l != nil; l = tl l){
		f := hd l;
		if(f.fid == fid){
			cp.funlock();
			if(flag)
				return nil;	# fid in use
			f.lock();
			if(f.fid == fid)
				return f;
			f.unlock();
			cp.flock();
		}
	}
	if(flag == 0){
		sys->print("kfs: cannot find %H.%ud", cp, fid);
		cp.funlock();
		return nil;
	}
	f := File.new(fid);
	f.lock();
	cp.flist = f :: cp.flist;
	cp.funlock();
	return f;
}

Chan.putfid(cp: self ref Chan, f: ref File)
{
	cp.flock();
	nl: list of ref File;
	for(x := cp.flist; x != nil; x = tl x)
		if(hd x != f)
			nl = hd x :: nl;
	cp.flist = nl;
	cp.funlock();
	f.unlock();
}

File.lock(f: self ref File)
{
	f.qlock <-= 1;
}

File.unlock(f: self ref File)
{
	<-f.qlock;
}

Chan.new(fd: ref Sys->FD): ref Chan
{
	c := ref Chan;
	c.fd = fd;
	c.fqlock = chan[1] of int;
#	rlock, wlock: QLock;		# lock for reading/writing messages on cp
	c.flags = 0;
#	reflock:	RWLock;		# lock for Tflush
	c.msize = 0;	# set by rversion
	return c;
}

Chan.flock(c: self ref Chan)
{
	c.fqlock <-= 1;
}

Chan.funlock(c: self ref Chan)
{
	<-c.fqlock;
}

rattach(cp: ref Chan, t: ref Tmsg.Attach): ref Rmsg
{
	if(t.aname != "" && t.aname != "main")
		return err(t, Ebadspc);
	file := cp.getfid(t.fid, 1);
	if(file == nil)
		return err(t, Efidinuse);
	p := Iobuf.get(thedevice, ROOTADDR, Bread);
	if(p == nil){
		cp.putfid(file);
		return err(t, "can't access root block");
	}
	d := Dentry.get(p, 0);
	if(d == nil || p.checktag(Tdir, QPROOT) || (d.mode & DALLOC) == 0 || (d.mode & DDIR) == 0){
		p.put();
		cp.putfid(file);
		return err(t, Ealloc);
	}
	if(file.access(d, DEXEC)){
		p.put();
		cp.putfid(file);
		return err(t, Eaccess);
	}
	d.access(FREAD, file.uid);
	file.fs = thedevice;
	file.qid = d.qid;
	file.addr = p.addr;
	file.slot = 0;
	file.open = 0;
	file.uid = strtouid(t.uname);
	file.wpath = nil;
	p.put();
	qid := file.qid;
	file.unlock();
	return ref Rmsg.Attach(t.tag, qid);
}

clone(nfile: ref File, file: ref File)
{
	nfile.qid = file.qid;
	nfile.wpath = file.wpath;
	nfile.fs = file.fs;
	nfile.addr = file.addr;
	nfile.slot = file.slot;
	nfile.uid = file.uid;
#	nfile.cuid = None;
	nfile.open = file.open & ~FREMOV;
}

walkname(file: ref File, wname: string): (string, Qid)
{
	#
	# File must not have been opened for I/O by an open
	# or create message and must represent a directory.
	#
	if(file.open != 0)
		return (Emode, noqid);

	(d, e) := Dentry.getd(file, Bread);
	if(d == nil)
		return (e, noqid);
	if(!(d.mode & DDIR)){
		d.put();
		return (Edir1, noqid);
	}

	#
	# For walked elements the implied user must
	# have permission to search the directory.
	#
	if(file.access(d, DEXEC)){
		d.put();
		return (Eaccess, noqid);
	}
	d.access(FREAD, file.uid);

	if(wname == "." || wname == ".." && file.wpath == nil){
		d.put();
		return (nil, file.qid);
	}

	d1: ref Dentry;	# entry for wname, if found
	slot: int;

	if(wname == ".."){
		d.put();
		addr := file.wpath.addr;
		slot = file.wpath.slot;
		(d1, e) = Dentry.geta(file.fs, addr, slot, QPNONE, Bread);
		if(d1 == nil)
			return (e, noqid);
		file.wpath = file.wpath.up;
	}else{

	Search:
		for(addr := 0; ; addr++){
			if(d.iob == nil){
				(d, e) = Dentry.getd(file, Bread);
				if(d == nil)
					return (e, noqid);
			}
			p1 := d.getblk1(addr, 0);
			if(p1 == nil || p1.checktag(Tdir, int d.qid.path)){
				if(p1 != nil)
					p1.put();
				return (Eentry, noqid);
			}
			for(slot = 0; slot < DIRPERBUF; slot++){
				d1 = Dentry.get(p1, slot);
				if(!(d1.mode & DALLOC))
					continue;
				if(wname != d1.name)
					continue;
				#
				# update walk path
				#
				file.wpath = ref Wpath(file.wpath, file.addr, file.slot);
				slot += DIRPERBUF*addr;
				break Search;
			}
			p1.put();
		}
		d.put();
	}

	file.addr = d1.iob.addr;
	file.slot = slot;
	file.qid = d1.qid;
	d1.put();
	return (nil, file.qid);
}

rwalk(cp: ref Chan, t: ref Tmsg.Walk): ref Rmsg
{
	nfile, tfile: ref File;
	q: Qid;

	# The file identified by t.fid must be valid in the
	# current session and must not have been opened for I/O
	# by an open or create message.

	if((file := cp.getfid(t.fid, 0)) == nil)
		return err(t, Efid);
	if(file.open != 0)
		return ferr(t, Emode, file, nil);

	# If newfid is not the same as fid, allocate a new file;
	# a side effect is checking newfid is not already in use (error);
	# if there are no names to walk this will be equivalent to a
	# simple 'clone' operation.
	# Otherwise, fid and newfid are the same and if there are names
	# to walk make a copy of 'file' to be used during the walk as
	# 'file' must only be updated on success.
	# Finally, it's a no-op if newfid is the same as fid and t.nwname
	# is 0.

	nwqid := 0;
	if(t.newfid != t.fid){
		if((nfile = cp.getfid(t.newfid, 1)) == nil)
			return ferr(t, Efidinuse, file, nil);
	}
	else if(len t.names != 0)
		nfile = tfile = File.new(NOFID);
	else{
		file.unlock();
		return ref Rmsg.Walk(t.tag, nil);
	}
	clone(nfile, file);

	r := ref Rmsg.Walk(t.tag, array[len t.names] of Qid);
	error: string;
	for(nwname := 0; nwname < len t.names; nwname++){
		(error, q) = walkname(nfile, t.names[nwname]);
		if(error != nil)
			break;
		r.qids[nwqid++] = q;
	}

	if(len t.names == 0){

		# Newfid must be different to fid (see above)
		# so this is a simple 'clone' operation - there's
		# nothing to do except unlock unless there's
		# an error.

		nfile.unlock();
		if(error != nil)
			cp.putfid(nfile);
	}else if(nwqid < len t.names){
		#
		# Didn't walk all elements, 'clunk' nfile
		# and leave 'file' alone.
		# Clear error if some of the elements were
		# walked OK.
		#
		if(nfile != tfile)
			cp.putfid(nfile);
		if(nwqid != 0)
			error = nil;
		r.qids = r.qids[0:nwqid];
	}else{
		#
		# Walked all elements. If newfid is the same
		# as fid must update 'file' from the temporary
		# copy used during the walk.
		# Otherwise just unlock (when using tfile there's
		# no need to unlock as it's a local).
		#
		if(nfile == tfile){
			file.qid = nfile.qid;
			file.wpath = nfile.wpath;
			file.addr = nfile.addr;
			file.slot = nfile.slot;
		}else
			nfile.unlock();
	}
	file.unlock();

	if(error != nil)
		return err(t, error);
	return r;
}

ropen(cp: ref Chan, f: ref Tmsg.Open): ref Rmsg
{
	wok := cp == conschan || writeallow;

	if((file := cp.getfid(f.fid, 0)) == nil)
		return err(f, Efid);

	#
	# if remove on close, check access here
	#
	ro := isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup));
	if(f.mode & ORCLOSE){
		if(ro)
			return ferr(f, Eronly, file, nil);
		#
		# check on parent directory of file to be deleted
		#
		if(file.wpath == nil || file.wpath.addr == file.addr)
			return ferr(f, Ephase, file, nil);
		p := Iobuf.get(file.fs, file.wpath.addr, Bread);
		if(p == nil || p.checktag(Tdir, QPNONE))
			return ferr(f, Ephase, file, p);
		if((d := Dentry.get(p, file.wpath.slot)) == nil || !(d.mode & DALLOC))
			return ferr(f, Ephase, file, p);
		if(file.access(d, DWRITE))
			return ferr(f, Eaccess, file, p);
		p.put();
	}
	(d, e) := Dentry.getd(file, Bread);
	if(d == nil)
		return ferr(f, e, file, nil);
	p := d.iob;
	qid := d.qid;
	fmod: int;
	case f.mode & 7 {

	OREAD =>
		if(file.access(d, DREAD) && !wok)
			return ferr(f, Eaccess, file, p);
		fmod = FREAD;

	OWRITE =>
		if((d.mode & DDIR) || (file.access(d, DWRITE) && !wok))
			return ferr(f, Eaccess, file, p);
		if(ro)
			return ferr(f, Eronly, file, p);
		fmod = FWRITE;

	ORDWR =>
		if((d.mode & DDIR)
		|| (file.access(d, DREAD) && !wok)
		|| (file.access(d, DWRITE) && !wok))
			return ferr(f, Eaccess, file, p);
		if(ro)
			return ferr(f, Eronly, file, p);
		fmod = FREAD+FWRITE;

	OEXEC =>
		if((d.mode & DDIR) || (file.access(d, DEXEC) && !wok))
			return ferr(f, Eaccess, file, p);
		fmod = FREAD;

	* =>
		return ferr(f, Emode, file, p);
	}
	if(f.mode & OTRUNC){
		if((d.mode & DDIR) || (file.access(d, DWRITE) && !wok))
			return ferr(f, Eaccess, file, p);
		if(ro)
			return ferr(f, Eronly, file, p);
	}
	if(d.mode & DLOCK){
		if((t := tlocked(file, d)) == nil)
			return ferr(f, Elocked, file, p);
		file.tlock = t;
		t.file = file;
	}
	if(f.mode & ORCLOSE)
		fmod |= FREMOV;
	file.open = fmod;
	if((f.mode & OTRUNC) && !(d.mode & DAPND)){
		d.trunc(file.uid);
		qid.vers = d.qid.vers;
	}
	file.lastra = 1;
	p.put();
	file.unlock();
	return ref Rmsg.Open(f.tag, qid, cp.msize-IOHDRSZ);
}

rcreate(cp: ref Chan, f: ref Tmsg.Create): ref Rmsg
{
	wok := cp == conschan || writeallow;

	if((file := cp.getfid(f.fid, 0)) == nil)
		return err(f, Efid);
	if(isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup)))
		return ferr(f, Eronly, file, nil);

	(d, e) := Dentry.getd(file, Bread);
	if(e != nil)
		return ferr(f, e, file, nil);
	p := d.iob;
	if(!(d.mode & DDIR))
		return ferr(f, Edir2, file, p);
	if(file.access(d, DWRITE) && !wok)
		return ferr(f, Eaccess, file, p);
	d.access(FREAD, file.uid);

	#
	# Check the name is valid and will fit in an old
	# directory entry.
	#
	if((l := checkname9p2(f.name)) == 0)
		return ferr(f, Ename, file, p);
	if(l+1 > NAMELEN)
		return ferr(f, Etoolong, file, p);
	if(f.name == "." || f.name == "..")
		return ferr(f, Edot, file, p);

	addr1 := 0;	# block with first empty slot, if any
	slot1 := 0;
	for(addr := 0; ; addr++){
		if((p1 := d.getblk(addr, 0)) == nil){
			if(addr1 != 0)
				break;
			p1 = d.getblk(addr, Tdir);
		}
		if(p1 == nil)
			return ferr(f, Efull, file, p);
		if(p1.checktag(Tdir, int d.qid.path)){
			p1.put();
			return ferr(f, Ephase, file, p);
		}
		for(slot := 0; slot < DIRPERBUF; slot++){
			d1 := Dentry.get(p1, slot);
			if(!(d1.mode & DALLOC)){
				if(addr1 == 0){
					addr1 = p1.addr;
					slot1 = slot + addr*DIRPERBUF;
				}
				continue;
			}
			if(f.name == d1.name){
				p1.put();
				return ferr(f, Eexist, file, p);
			}
		}
		p1.put();
	}

	fmod: int;

	case f.mode & 7 {
	OEXEC or
	OREAD =>		# seems only useful to make directories
		fmod = FREAD;

	OWRITE =>
		fmod = FWRITE;

	ORDWR =>
		fmod = FREAD+FWRITE;

	* =>
		return ferr(f, Emode, file, p);
	}
	if(f.perm & DMDIR)
		if((f.mode & OTRUNC) || (f.perm & DMAPPEND) || (fmod & FWRITE))
			return ferr(f, Eaccess, file, p);

	# do it

	path := qidpathgen(file.fs);
	if((p1 := Iobuf.get(file.fs, addr1, Bread|Bimm|Bmod)) == nil)
		return ferr(f, Ephase, file, p);
	d1 := Dentry.get(p1, slot1);
	if(d1 == nil || p1.checktag(Tdir, int d.qid.path)){
		p.put();
		return ferr(f, Ephase, file, p1);
	}
	if(d1.mode & DALLOC){
		p.put();
		return ferr(f, Ephase, file, p1);
	}

	d1.name = f.name;
	if(cp == conschan){
		d1.uid = consuid;
		d1.gid = consgid;
	}
	else{
		d1.uid = file.uid;
		d1.gid = d.gid;
		f.perm &= d.mode | ~8r666;
		if(f.perm & DMDIR)
			f.perm &= d.mode | ~8r777;
	}
	d1.qid.path = big path;
	d1.qid.vers = 0;
	d1.mode = DALLOC | (f.perm & 8r777);
	if(f.perm & DMDIR)
		d1.mode |= DDIR;
	if(f.perm & DMAPPEND)
		d1.mode |= DAPND;
	t: ref Tlock;
	if(f.perm & DMEXCL){
		d1.mode |= DLOCK;
		t = tlocked(file, d1);
		# if nil, out of tlock structures
	}
	d1.access(FWRITE, file.uid);
	d1.change(~0);
	d1.update();
	qid := mkqid(path, 0, d1.mode);
	p1.put();
	d.change(~0);
	d.access(FWRITE, file.uid);
	d.update();
	p.put();

	#
	# do a walk to new directory entry
	#
	file.wpath = ref Wpath(file.wpath, file.addr, file.slot);
	file.qid = qid;
	file.tlock = t;
	if(t != nil)
		t.file = file;
	file.lastra = 1;
	if(f.mode & ORCLOSE)
		fmod |= FREMOV;
	file.open = fmod;
	file.addr = addr1;
	file.slot = slot1;
	file.unlock();
	return ref Rmsg.Create(f.tag, qid, cp.msize-IOHDRSZ);
}

dirread(cp: ref Chan, f: ref Tmsg.Read, file: ref File, d: ref Dentry): ref Rmsg
{
	p1: ref Iobuf;
	d1: ref Dentry;

	count := f.count;
	data := array[count] of byte;
	offset := f.offset;
	iounit := cp.msize-IOHDRSZ;
	if(count > iounit)
		count = iounit;

	# Pick up where we left off last time if nothing has changed,
	# otherwise must scan from the beginning.

	addr, slot: int;
	start: big;

	if(offset == file.doffset){	# && file.qid.vers == file.dvers
		addr = file.dslot/DIRPERBUF;
		slot = file.dslot%DIRPERBUF;
		start = offset;
	}
	else{
		addr = 0;
		slot = 0;
		start = big 0;
	}

	nread := 0;
Dread:
	for(;;){
		if(d.iob == nil){
			#
			# This is just a check to ensure the entry hasn't
			# gone away during the read of each directory block.
			#
			e: string;
			(d, e) = Dentry.getd(file, Bread);
			if(d == nil)
				return ferr(f, e, file, nil);
		}
		p1 = d.getblk1(addr, 0);
		if(p1 == nil)
			break;
		if(p1.checktag(Tdir, QPNONE))
			return ferr(f, Ephase, file, p1);

		for(; slot < DIRPERBUF; slot++){
			d1 = Dentry.get(p1, slot);
			if(!(d1.mode & DALLOC))
				continue;
			dir := dir9p2(d1);
			n := styx->packdirsize(dir);
			if(n > count-nread){
				p1.put();
				break Dread;
			}
			data[nread:] = styx->packdir(dir);
			start += big n;
			if(start < offset)
				continue;
			if(count < n){
				p1.put();
				break Dread;
			}
			count -= n;
			nread += n;
			offset += big n;
		}
		p1.put();
		slot = 0;
		addr++;
	}

	file.doffset = offset;
	file.dvers = file.qid.vers;
	file.dslot = slot+DIRPERBUF*addr;

	d.put();
	file.unlock();
	return ref Rmsg.Read(f.tag, data[0:nread]);
}

rread(cp: ref Chan, f: ref Tmsg.Read): ref Rmsg
{
	if((file := cp.getfid(f.fid, 0)) == nil)
		return err(f, Efid);
	if(!(file.open & FREAD))
		return ferr(f, Eopen, file, nil);
	count := f.count;
	iounit := cp.msize-IOHDRSZ;
	if(count < 0 || count > iounit)
		return ferr(f, Ecount, file, nil);
	offset := f.offset;
	if(offset < big 0)
		return ferr(f, Eoffset, file, nil);

	(d, e) := Dentry.getd(file, Bread);
	if(d == nil)
		return ferr(f, e, file, nil);
	if((t := file.tlock) != nil){
		tim := now();
		if(t.time < tim || t.file != file){
			d.put();
			return ferr(f, Ebroken, file, nil);
		}
		# renew the lock
		t.time = tim + TLOCK;
	}
	d.access(FREAD, file.uid);
	if(d.mode & DDIR)
		return dirread(cp, f, file, d);

	if(offset+big count > d.size)
		count = int (d.size - offset);
	if(count < 0)
		count = 0;
	data := array[count] of byte;
	nread := 0;
	while(count > 0){
		if(d.iob == nil){
			# must check and reacquire entry
			(d, e) = Dentry.getd(file, Bread);
			if(d == nil)
				return ferr(f, e, file, nil);
		}
		addr := int (offset / big BUFSIZE);
		if(addr == file.lastra+1)
			;	# dbufread(p, d, addr+1);
		file.lastra = addr;
		o := int (offset % big BUFSIZE);
		n := BUFSIZE - o;
		if(n > count)
			n = count;
		p1 := d.getblk1(addr, 0);
		if(p1 != nil){
			if(p1.checktag(Tfile, QPNONE)){
				p1.put();
				return ferr(f, Ephase, file, nil);
			}
			data[nread:] = p1.iobuf[o:o+n];
			p1.put();
		}else
			data[nread:] = emptyblock[0:n];
		count -= n;
		nread += n;
		offset += big n;
	}
	d.put();
	file.unlock();
	return ref Rmsg.Read(f.tag, data[0:nread]);
}

rwrite(cp: ref Chan, f: ref Tmsg.Write): ref Rmsg
{
	if((file := cp.getfid(f.fid, 0)) == nil)
		return err(f, Efid);
	if(!(file.open & FWRITE))
		return ferr(f, Eopen, file, nil);
	if(isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup)))
		return ferr(f, Eronly, file, nil);
	count := len f.data;
	if(count < 0 || count > cp.msize-IOHDRSZ)
		return ferr(f, Ecount, file, nil);
	offset := f.offset;
	if(offset < big 0)
		return ferr(f, Eoffset, file, nil);

	(d, e) := Dentry.getd(file, Bread|Bmod);
	if(d == nil)
		return ferr(f, e, file, nil);
	if((t := file.tlock) != nil){
		tim := now();
		if(t.time < tim || t.file != file){
			d.put();
			return ferr(f, Ebroken, file, nil);
		}
		# renew the lock
		t.time = tim + TLOCK;
	}
	d.access(FWRITE, file.uid);
	if(d.mode & DAPND)
		offset = d.size;
	end := offset + big count;
	if(end > d.size){
		if(end > MAXFILESIZE)
			return ferr(f, Etoobig, file, nil);
		d.size = end;
		d.change(Usize);
	}
	d.update();

	nwrite := 0;
	while(count > 0){
		if(d.iob == nil){
			# must check and reacquire entry
			(d, e) = Dentry.getd(file, Bread|Bmod);
			if(d == nil)
				return ferr(f, e, file, nil);
		}
		addr := int (offset / big BUFSIZE);
		o := int (offset % big BUFSIZE);
		n := BUFSIZE - o;
		if(n > count)
			n = count;
		qpath := int d.qid.path;
		p1 := d.getblk1(addr, Tfile);
		if(p1 == nil)
			return ferr(f, Efull, file, nil);
		if(p1.checktag(Tfile, qpath)){
			p1.put();
			return ferr(f, Ealloc, file, nil);
		}
		p1.iobuf[o:] = f.data[nwrite:nwrite+n];
		p1.flags |= Bmod;
		p1.put();
		count -= n;
		nwrite += n;
		offset += big n;
	}
	d.put();
	file.unlock();
	return ref Rmsg.Write(f.tag, nwrite);
}

doremove(f: ref File, iscon: int): string
{
	if(isro(f.fs) || f.cons == 0 && (writegroup && !ingroup(f.uid, writegroup)))
		return Eronly;
	#
	# check permission on parent directory of file to be deleted
	#
	if(f.wpath == nil || f.wpath.addr == f.addr)
		return Ephase;
	(d1, e1) := Dentry.geta(f.fs, f.wpath.addr, f.wpath.slot, QPNONE, Bread);
	if(e1 != nil)
		return e1;
	if(!iscon && f.access(d1, DWRITE)){
		d1.put();
		return Eaccess;
	}
	d1.access(FWRITE, f.uid);
	d1.put();

	#
	# check on file to be deleted
	#
	(d, e) := Dentry.getd(f, Bread);
	if(e != nil)
		return e;

	#
	# if deleting a directory, make sure it is empty
	#
	if(d.mode & DDIR)
	for(addr:=0; (p1 := d.getblk(addr, 0)) != nil; addr++){
		if(p1.checktag(Tdir, int d.qid.path)){
			p1.put();
			d.put();
			return Ephase;
		}
		for(slot:=0; slot<DIRPERBUF; slot++){
			d1 = Dentry.get(p1, slot);
			if(!(d1.mode & DALLOC))
				continue;
			p1.put();
			d.put();
			return Eempty;
		}
		p1.put();
	}

	#
	# do it
	#
	d.trunc(f.uid);
	d.buf[0:] = emptyblock[0:Dentrysize];
	d.put();
	return nil;
}

clunk(cp: ref Chan, file: ref File, remove: int, wok: int): string
{
	if((t := file.tlock) != nil){
		if(t.file == file)
			t.time = 0;		# free the lock
		file.tlock = nil;
	}
	if(remove)
		error := doremove(file, wok);
	file.open = 0;
	file.wpath = nil;
	cp.putfid(file);

	return error;
}

rclunk(cp: ref Chan, t: ref Tmsg.Clunk): ref Rmsg
{
	if((file := cp.getfid(t.fid, 0)) == nil)
		return err(t, Efid);
	clunk(cp, file, file.open & FREMOV, 0);
	return ref Rmsg.Clunk(t.tag);
}

rremove(cp: ref Chan, t: ref Tmsg.Remove): ref Rmsg
{
	if((file := cp.getfid(t.fid, 0)) == nil)
		return err(t, Efid);
	e :=  clunk(cp, file, 1, cp == conschan);
	if(e != nil)
		return err(t, e);
	return ref Rmsg.Remove(t.tag);
}

rstat(cp: ref Chan, f: ref Tmsg.Stat): ref Rmsg
{
	if((file := cp.getfid(f.fid, 0)) == nil)
		return err(f, Efid);
	(d, e) := Dentry.getd(file, Bread);
	if(d == nil)
		return ferr(f, e, file, nil);
	dir := dir9p2(d);
	if(d.qid.path == big QPROOT)	# stat of root gives time
		dir.atime = now();
	d.put();
	if(styx->packdirsize(dir) > cp.msize-IOHDRSZ)
		return ferr(f, Ersc, file, nil);
	file.unlock();

	return ref Rmsg.Stat(f.tag, dir);
}

rwstat(cp: ref Chan, f: ref Tmsg.Wstat): ref Rmsg
{
	if((file := cp.getfid(f.fid, 0)) == nil)
		return err(f, Efid);

	# if user none, can't do anything unless in allow mode

	if(file.uid == None && !wstatallow)
		return ferr(f, Eaccess, file, nil);

	if(isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup)))
		return ferr(f, Eronly, file, nil);

	#
	# first get parent
	#
	p1: ref Iobuf;
	d1: ref Dentry;
	if(file.wpath != nil){
		p1 = Iobuf.get(file.fs, file.wpath.addr, Bread);
		if(p1 == nil)
			return ferr(f, Ephase, file, p1);
		d1 = Dentry.get(p1, file.wpath.slot);
		if(d1 == nil || p1.checktag(Tdir, QPNONE) || !(d1.mode & DALLOC))
			return ferr(f, Ephase, file, p1);
	}

	#
	# now the file
	#
	(d, e) := Dentry.getd(file, Bread);
	if(d == nil)
		return ferr(f, e, file, p1);

	#
	# Convert the message and fix up
	# fields not to be changed.
	#
	dir := f.stat;
	if(dir.uid == nil)
		uid := d.uid;
	else
		uid = strtouid(dir.uid);
	if(dir.gid == nil)
		gid := d.gid;
	else
		gid = strtouid(dir.gid);
	if(dir.name == nil)
		dir.name = d.name;
	else{
		if((l := checkname9p2(dir.name)) == 0){
			d.put();
			return ferr(f, Ename, file, p1);
		}
		if(l+1 > NAMELEN){
			d.put();
			return ferr(f, Etoolong, file, p1);
		}
	}

	# Before doing sanity checks, find out what the
	# new 'mode' should be:
	# if 'type' and 'mode' are both defaults, take the
	# new mode from the old directory entry;
	# else if 'type' is the default, use the new mode entry;
	# else if 'mode' is the default, create the new mode from
	# 'type' or'ed with the old directory mode;
	# else neither are defaults, use the new mode but check
	# it agrees with 'type'.

	if(dir.qid.qtype == 16rFF && dir.mode == ~0){
		dir.mode = d.mode & 8r777;
		if(d.mode & DLOCK)
			dir.mode |= DMEXCL;
		if(d.mode & DAPND)
			dir.mode |= DMAPPEND;
		if(d.mode & DDIR)
			dir.mode |= DMDIR;
	}
	else if(dir.qid.qtype == 16rFF){
		# nothing to do
	}
	else if(dir.mode == ~0)
		dir.mode = (dir.qid.qtype<<24)|(d.mode & 8r777);
	else if(dir.qid.qtype != ((dir.mode>>24) & 16rFF)){
		d.put();
		return ferr(f, Eqidmode, file, p1);
	}

	# Check for unknown type/mode bits
	# and an attempt to change the directory bit.

	if(dir.mode & ~(DMDIR|DMAPPEND|DMEXCL|8r777)){
		d.put();
		return ferr(f, Enotm, file, p1);
	}
	if(d.mode & DDIR)
		mode := DMDIR;
	else
		mode = 0;
	if((dir.mode^mode) & DMDIR){
		d.put();
		return ferr(f, Enotd, file, p1);
	}

	if(dir.mtime == ~0)
		dir.mtime = d.mtime;
	if(dir.length == ~big 0)
		dir.length = big d.size;


	# Currently, can't change length.

	if(dir.length != big d.size){
		d.put();
		return ferr(f, Enotl, file, p1);
	}


	# if chown,
	# must be god
	# wstatallow set to allow chown during boot

	if(uid != d.uid && !wstatallow){
		d.put();
		return ferr(f, Enotu, file, p1);
	}

	# if chgroup,
	# must be either
	#	a) owner and in new group
	#	b) leader of both groups
	# wstatallow and writeallow are set to allow chgrp during boot

	while(gid != d.gid){
		if(wstatallow || writeallow)
			break;
		if(d.uid == file.uid && ingroup(file.uid, gid))
			break;
		if(leadgroup(file.uid, gid))
			if(leadgroup(file.uid, d.gid))
				break;
		d.put();
		return ferr(f, Enotg, file, p1);
	}

	# if rename,
	# must have write permission in parent

	while(d.name != dir.name){

		# drop entry to prevent deadlock, then
		# check that destination name is valid and unique

		d.put();
		if(checkname9p2(dir.name) == 0 || d1 == nil)
			return ferr(f, Ename, file, p1);
		if(dir.name == "." || dir.name == "..")
			return ferr(f, Edot, file, p1);


		for(addr := 0; ; addr++){
			if((p := d1.getblk(addr, 0)) == nil)
				break;
			if(p.checktag(Tdir, int d1.qid.path)){
				p.put();
				continue;
			}
			for(slot := 0; slot < DIRPERBUF; slot++){
				d = Dentry.get(p, slot);
				if(!(d.mode & DALLOC))
					continue;
				if(dir.name == d.name){
					p.put();
					return ferr(f, Eexist, file, p1);
				}
			}
			p.put();
		}

		# reacquire entry

		(d, nil) = Dentry.getd(file, Bread);
		if(d == nil)
			return ferr(f, Ephase, file, p1);

		if(wstatallow || writeallow) # set to allow rename during boot
			break;
		if(d1 == nil || file.access(d1, DWRITE)){
			d.put();
			return ferr(f, Eaccess, file, p1);
		}
		break;
	}

	# if mode/time, either
	#	a) owner
	#	b) leader of either group

	mode = dir.mode & 8r777;
	if(dir.mode & DMAPPEND)
		mode |= DAPND;
	if(dir.mode & DMEXCL)
		mode |= DLOCK;
	while(d.mtime != dir.mtime || ((d.mode^mode) & (DAPND|DLOCK|8r777))){
		if(wstatallow)			# set to allow chmod during boot
			break;
		if(d.uid == file.uid)
			break;
		if(leadgroup(file.uid, gid))
			break;
		if(leadgroup(file.uid, d.gid))
			break;
		d.put();
		return ferr(f, Enotu, file, p1);
	}
	d.mtime = dir.mtime;
	d.uid = uid;
	d.gid = gid;
	d.mode = (mode & (DAPND|DLOCK|8r777)) | (d.mode & (DALLOC|DDIR));

	d.name = dir.name;
	d.access(FWSTAT, file.uid);
	d.change(~0);
	d.put();

	if(p1 != nil)
		p1.put();
	file.unlock();

	return ref Rmsg.Wstat(f.tag);
}

superok(set: int): int
{
	sb := Superb.get(thedevice, Bread|Bmod|Bimm);
	ok := sb.fsok;
	sb.fsok = set;
	if(debug)
		sb.print();
	sb.touched();
	sb.put();
	return ok;
}

# little-endian
get2(a: array of byte, o: int): int
{
	return (int a[o+1]<<8) | int a[o];
}

get2s(a: array of byte, o: int): int
{
	v := (int a[o+1]<<8) | int a[o];
	if(v & 16r8000)
		v |= ~0 << 8;
	return v;
}

get4(a: array of byte, o: int): int
{
	return (int a[o+3]<<24) | (int a[o+2] << 16) | (int a[o+1]<<8) | int a[o];
}

put2(a: array of byte, o: int, v: int)
{
	a[o] = byte v;
	a[o+1] = byte (v>>8);
}

put4(a: array of byte, o: int, v: int)
{
	a[o] = byte v;
	a[o+1] = byte (v>>8);
	a[o+2] = byte (v>>16);
	a[o+3] = byte (v>>24);
}

Tag.unpack(a: array of byte): Tag
{
	return Tag(get2(a,2), get4(a,4));
}

Tag.pack(t: self Tag, a: array of byte)
{
	put2(a, 0, 0);
	put2(a, 2, t.tag);
	if(t.path != QPNONE)
		put4(a, 4, t.path & ~QPDIR);
}

Superb.get(dev: ref Device, flags: int): ref Superb
{
	p := Iobuf.get(dev, SUPERADDR, flags);
	if(p == nil)
		return nil;
	if(p.checktag(Tsuper, QPSUPER)){
		p.put();
		return nil;
	}
	sb := Superb.unpack(p.iobuf);
	sb.iob = p;
	return sb;
}

Superb.touched(s: self ref Superb)
{
	s.iob.flags |= Bmod;
}

Superb.put(sb: self ref Superb)
{
	if(sb.iob == nil)
		return;
	if(sb.iob.flags & Bmod)
		sb.pack(sb.iob.iobuf);
	sb.iob.put();
	sb.iob = nil;
}

#  this is the disk structure
# Superb:
#	Super1;
#	Fbuf	fbuf;
# Fbuf:
#	nfree[4]
#	free[]	# based on BUFSIZE
#  Super1:
#	long	fstart;
#	long	fsize;
#	long	tfree;
#	long	qidgen;		# generator for unique ids
#	long	fsok;		# file system ok
#	long	roraddr;	# dump root addr
#	long	last;		# last super block addr
#	long	next;		# next super block addr

Ofstart: con 0;
Ofsize: con Ofstart+4;
Otfree: con Ofsize+4;
Oqidgen: con Otfree+4;
Ofsok: con Oqidgen+4;
Ororaddr: con Ofsok+4;
Olast: con Ororaddr+4;
Onext: con Olast+4;
Super1size: con Onext+4;

Superb.unpack(a: array of byte): ref Superb
{
	s := ref Superb;
	s.fstart = get4(a, Ofstart);
	s.fsize = get4(a, Ofsize);
	s.tfree = get4(a, Otfree);
	s.qidgen = get4(a, Oqidgen);
	s.fsok = get4(a, Ofsok);
	s.fbuf = a[Super1size:];
	return s;
}

Superb.pack(s: self ref Superb, a: array of byte)
{
	put4(a, Ofstart, s.fstart);
	put4(a, Ofsize, s.fsize);
	put4(a, Otfree, s.tfree);
	put4(a, Oqidgen, s.qidgen);
	put4(a, Ofsok, s.fsok);
}

Superb.print(sb: self ref Superb)
{
	sys->print("fstart=%ud fsize=%ud tfree=%ud qidgen=%ud fsok=%d\n",
		sb.fstart, sb.fsize, sb.tfree, sb.qidgen, sb.fsok);
}

Dentry.get(p: ref Iobuf, slot: int): ref Dentry
{
	if(p == nil)
		return nil;
	buf := p.iobuf[(slot%DIRPERBUF)*Dentrysize:];
	d := Dentry.unpack(buf);
	d.iob = p;
	d.buf = buf;
	return d;
}

Dentry.geta(fs: ref Device, addr: int, slot: int, qpath: int, mode: int): (ref Dentry, string)
{
	p := Iobuf.get(fs, addr, mode);
	if(p == nil || p.checktag(Tdir, qpath)){
		if(p != nil)
			p.put();
		return (nil, Ealloc);
	}
	d := Dentry.get(p, slot);
	if(d == nil || !(d.mode & DALLOC)){
		p.put();
		return (nil, Ealloc);
	}
	return (d, nil);
}

Dentry.getd(file: ref File, mode: int): (ref Dentry, string)
{
	(d, e) := Dentry.geta(file.fs, file.addr, file.slot, QPNONE, mode);	# QPNONE should be file.wpath's path
	if(e != nil)
		return (nil, e);
	if(file.qid.path != d.qid.path || (file.qid.qtype&QTDIR) != (d.qid.qtype&QTDIR)){
		d.put();
		return (nil, Eqid);
	}
	return (d, nil);
}

#  this is the disk structure:
#	char	name[NAMELEN];
#	short	uid;
#	short	gid;		[2*2]
#	ushort	mode;
#		#define	DALLOC	0x8000
#		#define	DDIR	0x4000
#		#define	DAPND	0x2000
#		#define	DLOCK	0x1000
#		#define	DREAD	0x4
#		#define	DWRITE	0x2
#		#define	DEXEC	0x1
#	[ushort muid]		[2*2]
#	Qid.path;			[4]
#	Qid.version;		[4]
#	long	size;			[4]
#	long	dblock[NDBLOCK];
#	long	iblock;
#	long	diblock;
#	long	atime;
#	long	mtime;

Oname: con 0;
Ouid: con Oname+NAMELEN;
Ogid: con Ouid+2;
Omode: con Ogid+2;
Omuid: con Omode+2;
Opath: con Omuid+2;
Overs: con Opath+4;
Osize: con Overs+4;
Odblock: con Osize+4;
Oiblock: con Odblock+NDBLOCK*4;
Odiblock: con Oiblock+4;
Oatime: con Odiblock+4;
Omtime: con Oatime+4;
Dentrysize: con Omtime+4;

Dentry.unpack(a: array of byte): ref Dentry
{
	d := ref Dentry;
	for(i:=0; i<NAMELEN; i++)
		if(int a[i] == 0)
			break;
	d.name = string a[0:i];
	d.uid = get2s(a, Ouid);
	d.gid = get2s(a, Ogid);
	d.mode = get2(a, Omode);
	d.muid = get2(a, Omuid);	# note: not set by Plan 9's kfs
	d.qid = mkqid(get4(a, Opath), get4(a, Overs), d.mode);
	d.size = big get4(a, Osize) & big 16rFFFFFFFF;
	d.atime = get4(a, Oatime);
	d.mtime = get4(a, Omtime);
	d.mod = 0;
	return d;
}

Dentry.change(d: self ref Dentry, f: int)
{
	d.mod |= f;
}

Dentry.update(d: self ref Dentry)
{
	f := d.mod;
	d.mod = 0;
	if(d.iob == nil || (d.iob.flags & Bmod) == 0){
		if(f != 0)
			panic("Dentry.update");
		return;
	}
	a := d.buf;
	if(f & Uname){
		b := array of byte d.name;
		for(i := 0; i < NAMELEN; i++)
			if(i < len b)
				a[i] = b[i];
			else
				a[i] = byte 0;
	}
	if(f & Uids){
		put2(a, Ouid, d.uid);
		put2(a, Ogid, d.gid);
	}
	if(f & Umode)
		put2(a, Omode, d.mode);
	if(f & Uqid){
		path := int d.qid.path;
		if(d.mode & DDIR)
			path |= QPDIR;
		put4(a, Opath, path);
		put4(a, Overs, d.qid.vers);
	}
	if(f & Usize)
		put4(a, Osize, int d.size);
	if(f & Utime){
		put4(a, Omtime, d.mtime);
		put4(a, Oatime, d.atime);
	}
	d.iob.flags |= Bmod;
}

Dentry.access(d: self ref Dentry, f: int, uid: int)
{
	if((p := d.iob) != nil && !readonly){
		if((f & (FWRITE|FWSTAT)) == 0 && noatime)
			return;
		if(f & (FREAD|FWRITE|FWSTAT)){
			d.atime = now();
			put4(d.buf, Oatime, d.atime);
			p.flags |= Bmod;
		}
		if(f & FWRITE){
			d.mtime = now();
			put4(d.buf, Omtime, d.mtime);
			d.muid = uid;
			put2(d.buf, Omuid, uid);
			d.qid.vers++;
			put4(d.buf, Overs, d.qid.vers);
			p.flags |= Bmod;
		}
	}
}

#
# release the directory entry buffer and thus the
# lock on both buffer and entry, typically during i/o,
# to be reacquired later if needed
#
Dentry.release(d: self ref Dentry)
{
	if(d.iob != nil){
		d.update();
		d.iob.put();
		d.iob = nil;
		d.buf = nil;
	}
}

Dentry.getblk(d: self ref Dentry, a: int, tag: int): ref Iobuf
{
	addr := d.rel2abs(a, tag, 0);
	if(addr == 0)
		return nil;
	return Iobuf.get(thedevice, addr, Bread);
}

#
# same as Dentry.buf but calls d.release
# to reduce interference.
#
Dentry.getblk1(d: self ref Dentry, a: int, tag: int): ref Iobuf
{
	addr := d.rel2abs(a, tag, 1);
	if(addr == 0)
		return nil;
	return Iobuf.get(thedevice, addr, Bread);
}

Dentry.rel2abs(d: self ref Dentry, a: int, tag: int, putb: int): int
{
	if(a < 0){
		sys->print("Dentry.rel2abs: neg\n");
		return 0;
	}
	p := d.iob;
	if(p == nil || d.buf == nil)
		panic("nil iob");
	data := d.buf;
	qpath := int d.qid.path;
	dev := p.dev;
	if(a < NDBLOCK){
		addr := get4(data, Odblock+a*4);
		if(addr == 0 && tag){
			addr = balloc(dev, tag, qpath);
			put4(data, Odblock+a*4, addr);
			p.flags |= Bmod|Bimm;
		}
		if(putb)
			d.release();
		return addr;
	}
	a -= NDBLOCK;
	if(a < INDPERBUF){
		addr := get4(data, Oiblock);
		if(addr == 0 && tag){
			addr = balloc(dev, Tind1, qpath);
			put4(data, Oiblock, addr);
			p.flags |= Bmod|Bimm;
		}
		if(putb)
			d.release();
		return  indfetch(dev, qpath, addr, a, Tind1, tag);
	}
	a -= INDPERBUF;
	if(a < INDPERBUF2){
		addr := get4(data, Odiblock);
		if(addr == 0 && tag){
			addr = balloc(dev, Tind2, qpath);
			put4(data, Odiblock, addr);
			p.flags |= Bmod|Bimm;
		}
		if(putb)
			d.release();
		addr = indfetch(dev, qpath, addr, a/INDPERBUF, Tind2, Tind1);
		return indfetch(dev, qpath, addr, a%INDPERBUF, Tind1, tag);
	}
	if(putb)
		d.release();
	sys->print("Dentry.buf: trip indirect\n");
	return 0;
}

indfetch(dev: ref Device, path: int, addr: int, a: int, itag: int, tag: int): int
{
	if(addr == 0)
		return 0;
	bp := Iobuf.get(dev, addr, Bread);
	if(bp == nil){
		sys->print("ind fetch bp = nil\n");
		return 0;
	}
	if(bp.checktag(itag, path)){
		sys->print("ind fetch tag\n");
		bp.put();
		return 0;
	}
	addr = get4(bp.iobuf, a*4);
	if(addr == 0 && tag){
		addr = balloc(dev, tag, path);
		if(addr != 0){
			put4(bp.iobuf, a*4, addr);
			bp.flags |= Bmod;
			if(localfs || tag == Tdir)
				bp.flags |= Bimm;
			bp.settag(itag, path);
		}
	}
	bp.put();
	return addr;
}

balloc(dev: ref Device, tag: int, qpath: int): int
{
	# TO DO: cache superblock to reduce pack/unpack
	sb := Superb.get(dev, Bread|Bmod);
	if(sb == nil)
		panic("balloc: super block");
	n := get4(sb.fbuf, 0);
	n--;
	sb.tfree--;
	if(n < 0 || n >= FEPERBUF)
		panic("balloc: bad freelist");
	a := get4(sb.fbuf, 4+n*4);
	if(n == 0){
		if(a == 0){
			sb.tfree = 0;
			sb.touched();
			sb.put();
			return 0;
		}
		bp := Iobuf.get(dev, a, Bread);
		if(bp == nil || bp.checktag(Tfree, QPNONE)){
			if(bp != nil)
				bp.put();
			sb.put();
			return 0;
		}
		sb.fbuf[0:] = bp.iobuf[0:(FEPERBUF+1)*4];
		sb.touched();
		bp.put();
	}else{
		put4(sb.fbuf, 0, n);
		sb.touched();
	}
	bp := Iobuf.get(dev, a, Bmod);
	bp.iobuf[0:] = emptyblock;
	bp.settag(tag, qpath);
	if(tag == Tind1 || tag == Tind2 || tag == Tdir)
		bp.flags |= Bimm;
	bp.put();
	sb.put();
	return a;
}

bfree(dev: ref Device, addr: int, d: int)
{
	if(addr == 0)
		return;
	if(d > 0){
		d--;
		p := Iobuf.get(dev, addr, Bread);
		if(p != nil){
			for(i:=INDPERBUF-1; i>=0; i--){
				a := get4(p.iobuf, i*4);
				bfree(dev, a, d);
			}
			p.put();
		}
	}

	# stop outstanding i/o
	p := Iobuf.get(dev, addr, Bprobe);
	if(p != nil){
		p.flags &= ~(Bmod|Bimm);
		p.put();
	}

	s := Superb.get(dev, Bread|Bmod);
	if(s == nil)
		panic("bfree: super block");
	addfree(dev, addr, s);
	s.put();
}

addfree(dev: ref Device, addr: int, sb: ref Superb)
{
	if(addr >= sb.fsize){
		sys->print("addfree: bad addr %ud\n", addr);
		return;
	}
	n := get4(sb.fbuf, 0);
	if(n < 0 || n > FEPERBUF)
		panic("addfree: bad freelist");
	if(n >= FEPERBUF){
		p := Iobuf.get(dev, addr, Bmod);
		if(p == nil)
			panic("addfree: Iobuf.get");
		p.iobuf[0:] = sb.fbuf[0:(1+FEPERBUF)*4];
		sb.fbuf[0:] = emptyblock[0:(1+FEPERBUF)*4];	# clear it for debugging
		p.settag(Tfree, QPNONE);
		p.put();
		n = 0;
	}
	put4(sb.fbuf, 4+n*4, addr);
	put4(sb.fbuf, 0, n+1);
	sb.tfree++;
	if(addr >= sb.fsize)
		sb.fsize = addr+1;
	sb.touched();
}

qidpathgen(dev: ref Device): int
{
	sb := Superb.get(dev, Bread|Bmod);
	if(sb == nil)
		panic("qidpathgen: super block");
	sb.qidgen++;
	path := sb.qidgen;
	sb.touched();
	sb.put();
	return path;
}

Dentry.trunc(d: self ref Dentry, uid: int)
{
	p := d.iob;
	data := d.buf;
	bfree(p.dev, get4(data, Odiblock), 2);
	put4(data, Odiblock, 0);
	bfree(p.dev, get4(data, Oiblock), 1);
	put4(data, Oiblock, 0);
	for(i:=NDBLOCK-1; i>=0; i--){
		bfree(p.dev, get4(data, Odblock+i*4), 0);
		put4(data, Odblock+i*4, 0);
	}
	d.size = big 0;
	d.change(Usize);
	p.flags |= Bmod|Bimm;
	d.access(FWRITE, uid);
	d.update();
}

Dentry.put(d: self ref Dentry)
{
	p := d.iob;
	if(p == nil || d.buf == nil)
		return;
	d.update();
	p.put();
	d.iob = nil;
	d.buf = nil;
}

Dentry.print(d: self ref Dentry)
{
	sys->print("name=%#q uid=%d gid=%d mode=#%8.8ux qid.path=#%bux qid.vers=%ud size=%bud\n",
		d.name, d.uid, d.gid, d.mode, d.qid.path, d.qid.vers, d.size);
	p := d.iob;
	if(p != nil && (data := p.iobuf) != nil){
		sys->print("\tdblock=");
		for(i := 0; i < NDBLOCK; i++)
			sys->print(" %d", get4(data, Odblock+i*4));
		sys->print(" iblock=%ud diblock=%ud\n", get4(data, Oiblock), get4(data, Odiblock));
	}
}

HWidth: con 5;	# buffers per line

hiob: array of ref Hiob;

iobufinit(niob: int)
{
	nhiob := niob/HWidth;
	while(!prime(nhiob))
		nhiob++;
	hiob = array[nhiob] of {* => ref Hiob(nil, Lock.new(), 0)};
	# allocate the buffers now
	for(i := 0; i < len hiob; i++){
		h := hiob[i];
		while(h.niob < HWidth)
			h.newbuf();
	}
}

iobufclear()
{
	# eliminate the cyclic references
	for(i := 0; i < len hiob; i++){
		h := hiob[i];
		while(--h.niob >= 0){
			p := hiob[i].link;
			hiob[i].link = p.fore;
			p.fore = p.back = nil;
			p = nil;
		}
	}
}

prime(n: int): int
{
	if((n%2) == 0)
		return 0;
	for(i:=3;; i+=2) {
		if((n%i) == 0)
			return 0;
		if(i*i >= n)
			return 1;
	}
}

Hiob.newbuf(hb: self ref Hiob): ref Iobuf
{
	# hb must be locked
	p := ref Iobuf;
	p.qlock = chan[1] of int;
	q := hb.link;
	if(q != nil){
		p.fore = q;
		p.back = q.back;
		q.back = p;
		p.back.fore = p;
	}else{
		hb.link = p;
		p.fore = p;
		p.back = p;
	}
	p.dev = devnone;
	p.addr = -1;
	p.flags = 0;
	p.xiobuf = array[RBUFSIZE] of byte;
	hb.niob++;
	return p;
}

Iobuf.get(dev: ref Device, addr: int, flags: int): ref Iobuf
{
	hb := hiob[addr%len hiob];
	p: ref Iobuf;
Search:
	for(;;){
		hb.lk.lock();
		s := hb.link;

		# see if it's active
		p = s;
		do{
			if(p.addr == addr && p.dev == dev){
				if(p != s){
					p.back.fore = p.fore;
					p.fore.back = p.back;
					p.fore = s;
					p.back = s.back;
					s.back = p;
					p.back.fore = p;
					hb.link = p;
				}
				hb.lk.unlock();
				p.lock();
				if(p.addr != addr || p.dev != dev){
					# lost race
					p.unlock();
					continue Search;
				}
				p.flags |= flags;
				p.iobuf = p.xiobuf;
				return p;
			}
		}while((p = p.fore) != s);
		if(flags == Bprobe){
			hb.lk.unlock();
			return nil;
		}

		# steal the oldest unlocked buffer
		do{
			p = s.back;
			if(p.canlock()){
				# TO DO: if Bmod, write it out and restart Hashed
				# for now we needn't because Iobuf.put is synchronous
				if(p.flags & Bmod)
					sys->print("Bmod unexpected (%ud)\n", p.addr);
				hb.link = p;
				p.dev = dev;
				p.addr = addr;
				p.flags = flags;
				break Search;
			}
			s = p;
		}while(p != hb.link);

		# no unlocked blocks available; add a new one
		p = hb.newbuf();
		p.lock();	# return it locked
		break;
	}

	p.dev = dev;
	p.addr = addr;
	p.flags = flags;
	hb.lk.unlock();
	p.iobuf = p.xiobuf;
	if(flags & Bread){
		if(wrenread(dev.fd, addr, p.iobuf)){
			eprint(sys->sprint("error reading block %ud: %r", addr));
			p.flags = 0;
			p.dev = devnone;
			p.addr = -1;
			p.iobuf = nil;
			p.unlock();
			return nil;
		}
	}
	return p;
}

Iobuf.put(p: self ref Iobuf)
{
	if(p.flags & Bmod)
		p.flags |= Bimm;	# temporary; see comment in Iobuf.get
	if(p.flags & Bimm){
		if(!(p.flags & Bmod))
			eprint(sys->sprint("imm and no mod (%d)", p.addr));
		if(!wrenwrite(p.dev.fd, p.addr, p.iobuf))
			p.flags &= ~(Bmod|Bimm);
		else
			panic(sys->sprint("error writing block %ud: %r", p.addr));
	}
	p.iobuf = nil;
	p.unlock();
}

Iobuf.lock(p: self ref Iobuf)
{
	p.qlock <-= 1;
}

Iobuf.canlock(p: self ref Iobuf): int
{
	alt{
	p.qlock <-= 1 =>
		return 1;
	* =>
		return 0;
	}
}

Iobuf.unlock(p: self ref Iobuf)
{
	<-p.qlock;
}

File.access(f: self ref File, d: ref Dentry, m: int): int
{
	if(wstatallow)
		return 0;

	# none gets only other permissions

	if(f.uid != None){
		if(f.uid == d.uid)	# owner
			if((m<<6) & d.mode)
				return 0;
		if(ingroup(f.uid, d.gid))	# group membership
			if((m<<3) & d.mode)
				return 0;
	}

	#
	# other access for everyone except members of group "noworld"
	#
	if(m & d.mode){
		#
		# walk directories regardless.
		# otherwise it's impossible to get
		# from the root to noworld's directories.
		#
		if((d.mode & DDIR) && (m == DEXEC))
			return 0;
		if(!ingroup(f.uid, Noworld))
			return 0;
	}
	return 1;
}

tagname(t: int): string
{
	case t {
	Tnone =>	return "Tnone";
	Tsuper =>	return "Tsuper";
	Tdir => return "Tdir";
	Tind1 => return "Tind1";
	Tind2 => return "Tind2";
	Tfile => return "Tfile";
	Tfree => return "Tfree";
	Tbuck => return "Tbuck";
	Tvirgo => return "Tvirgo";
	Tcache => return "Tcache";
	* =>	return sys->sprint("%d", t);
	}
}

Iobuf.checktag(p: self ref Iobuf, tag: int, qpath: int): int
{
	t := Tag.unpack(p.iobuf[BUFSIZE:]);
	if(t.tag != tag){
		if(1)
			eprint(sys->sprint("	tag = %s; expected %s; addr = %ud\n",
				tagname(t.tag), tagname(tag), p.addr));
		return 2;
	}
	if(qpath != QPNONE){
		qpath &= ~QPDIR;
		if(qpath != t.path){
			if(qpath == (t.path&~QPDIR))	# old bug
				return 0;
			if(1)
				eprint(sys->sprint("	tag/path = %ux; expected %s/%ux\n",
					t.path, tagname(tag), qpath));
			return 1;
		}
	}
	return 0;
}

Iobuf.settag(p: self ref Iobuf, tag: int, qpath: int)
{
	Tag(tag, qpath).pack(p.iobuf[BUFSIZE:]);
	p.flags |= Bmod;
}

badmagic := 0;
wmagic := "kfs wren device\n";

wrenream(dev: ref Device)
{
	if(RBUFSIZE % 512)
		panic(sys->sprint("kfs: bad buffersize(%d): restart a multiple of 512", RBUFSIZE));
	if(RBUFSIZE > MAXBUFSIZE)
		panic(sys->sprint("kfs: bad buffersize(%d): must be at most %d", RBUFSIZE, MAXBUFSIZE));
	sys->print("kfs: reaming the file system using %d byte blocks\n", RBUFSIZE);
	buf := array[RBUFSIZE] of {* => byte 0};
	buf[256:] = sys->aprint("%s%d\n", wmagic, RBUFSIZE);
	if(sys->seek(dev.fd, big 0, 0) < big 0 || sys->write(dev.fd, buf, len buf) != len buf)
		panic("can't ream disk");
}

wreninit(dev: ref Device): int
{
	(ok, nil) := sys->fstat(dev.fd);
	if(ok < 0)
		return 0;
	buf := array[MAXBUFSIZE] of byte;
	sys->seek(dev.fd, big 0, 0);
	n := sys->read(dev.fd, buf, len buf);
	if(n < len buf)
		return 0;
	badmagic = 0;
	RBUFSIZE = 1024;
	if(string buf[256:256+len wmagic] != wmagic){
		badmagic = 1;
		return 0;
	}
	RBUFSIZE = int string buf[256+len wmagic:256+len wmagic+12];
	if(RBUFSIZE % 512)
		error("bad block size");
	return 1;
}

wrenread(fd: ref Sys->FD, addr: int, a: array of byte): int
{
	return sys->pread(fd, a, len a, big addr * big RBUFSIZE) != len a;
}

wrenwrite(fd: ref Sys->FD, addr: int, a: array of byte): int
{
	return sys->pwrite(fd, a, len a, big addr * big RBUFSIZE) != len a;
}

wrentag(buf: array of byte, tag: int, qpath: int): int
{
	t := Tag.unpack(buf[BUFSIZE:]);
	return t.tag != tag || (qpath&~QPDIR) != t.path;
}

wrencheck(fd: ref Sys->FD): int
{
	if(badmagic)
		return 1;
	buf := array[RBUFSIZE] of byte;
	if(wrenread(fd, SUPERADDR, buf) || wrentag(buf, Tsuper, QPSUPER) ||
	    wrenread(fd, ROOTADDR, buf) || wrentag(buf, Tdir, QPROOT))
		return 1;
	d0 := Dentry.unpack(buf);
	if(d0.mode & DALLOC)
		return 0;
	return 1;
}

wrensize(dev: ref Device): int
{
	(ok, d) := sys->fstat(dev.fd);
	if(ok < 0)
		return -1;
	return int (d.length / big RBUFSIZE);
}

checkname9p2(s: string): int
{
	for(i := 0; i < len s; i++)
		if(s[i] <= 8r40)
			return 0;
	return styx->utflen(s);
}

isro(d: ref Device): int
{
	return d == nil || d.ronly;
}

tlocks: list of ref Tlock;

tlocked(f: ref File, d: ref Dentry): ref Tlock
{
	tim := now();
	path := int d.qid.path;
	t1: ref Tlock;
	for(l := tlocks; l != nil; l = tl l){
		t := hd l;
		if(t.qpath == path && t.time >= tim && t.dev == f.fs)
			return nil;	# it's locked
		if(t.file == nil || t1 == nil && t.time < tim)
			t1 = t;
	}
	t := t1;
	if(t == nil)
		t = ref Tlock;
	t.dev = f.fs;
	t.qpath = path;
	t.time = tim + TLOCK;
	tlocks = t :: tlocks;
	return t;
}

mkqid(path: int, vers: int, mode: int): Qid
{
	qid: Qid;

	qid.path = big (path & ~QPDIR);
	qid.vers = vers;
	qid.qtype = 0;
	if(mode & DDIR)
		qid.qtype |= QTDIR;
	if(mode & DAPND)
		qid.qtype |= QTAPPEND;
	if(mode & DLOCK)
		qid.qtype |= QTEXCL;
	return qid;
}

dir9p2(d: ref Dentry): Sys->Dir
{
	dir: Sys->Dir;

	dir.name = d.name;
	dir.uid = uidtostr(d.uid);
	dir.gid = uidtostr(d.gid);
	dir.muid = uidtostr(d.muid);
	dir.qid = d.qid;
	dir.mode = d.mode & 8r777;
	if(d.mode & DDIR)
		dir.mode |= DMDIR;
	if(d.mode & DAPND)
		dir.mode |= DMAPPEND;
	if(d.mode & DLOCK)
		dir.mode |= DMEXCL;
	dir.atime = d.atime;
	dir.mtime = d.mtime;
	dir.length = big d.size;
	dir.dtype = 0;
	dir.dev = 0;
	return dir;
}

rootream(dev: ref Device, addr: int)
{
	p := Iobuf.get(dev, addr, Bmod|Bimm);
	p.iobuf[0:] = emptyblock;
	p.settag(Tdir, QPROOT);
	d := Dentry.get(p, 0);
	d.name = "/";
	d.uid = -1;
	d.gid = -1;
	d.mode = DALLOC | DDIR |
		((DREAD|DWRITE|DEXEC) << 6) |
		((DREAD|DWRITE|DEXEC) << 3) |
		((DREAD|DWRITE|DEXEC) << 0);
	d.qid.path = big QPROOT;
	d.qid.vers = 0;
	d.qid.qtype = QTDIR;
	d.atime = now();
	d.mtime = d.atime;
	d.change(~0);
	d.access(FREAD|FWRITE, -1);
	d.update();
	p.put();
}

superream(dev: ref Device, addr: int)
{
	fsize := wrensize(dev);
	if(fsize <= 0)
		panic("file system device size");
	p := Iobuf.get(dev, addr, Bmod|Bimm);
	p.iobuf[0:] = emptyblock;
	p.settag(Tsuper, QPSUPER);
	sb := ref Superb;
	sb.iob = p;
	sb.fstart = 1;
	sb.fsize = fsize;
	sb.qidgen = 10;
	sb.tfree = 0;
	sb.fsok = 0;
	sb.fbuf = p.iobuf[Super1size:];
	put4(sb.fbuf, 0, 1);	# nfree = 1
	for(i := fsize-1; i>=addr+2; i--)
		addfree(dev, i, sb);
	sb.put();
}

eprint(s: string)
{
	sys->print("kfs: %s\n", s);
}

#
# /adm/users
#
# uid:user:leader:members[,...]

User: adt {
	uid:	int;
	name:	string;
	leader:	int;
	mem:	list of int;
};

users: list of ref User;

admusers := array[] of {
	(-1, "adm", "adm"),
	(None, "none", "adm"),
	(Noworld, "noworld", nil),
	(10000, "sys", nil),
	(10001, "upas", "upas"),
	(10002, "bootes", "bootes"),
	(10006, "inferno", nil),
};

userinit()
{
	if(!cmd_users() && users == nil){
		cprint("initializing minimal user table");
		defaultusers();
	}
	writegroup = strtouid("write");
}

cmd_users(): int
{
	if(kopen(FID1, FID2, array[] of {"adm", "users"}, OREAD) != nil)
		return 0;
	buf: array of byte;
	for(off := 0;;){
		(a, e) := kread(FID2, off, Styx->MAXFDATA);
		if(e != nil){
			cprint("/adm/users read error: "+e);
			return 0;
		}
		if(len a == 0)
			break;
		off += len a;
		if(buf != nil){
			c := array[len buf + len a] of byte;
			if(buf != nil)
				c[0:] = buf;
			c[len buf:] = a;
			buf = c;
		}else
			buf = a;
	}
	kclose(FID2);

	# (uid:name:lead:mem,...\n)+
	(nl, lines) := sys->tokenize(string buf, "\n");
	if(nl == 0){
		cprint("empty /adm/users");
		return 0;
	}
	oldusers := users;
	users = nil;

	# first pass: enter id:name
	for(l := lines; l != nil; l = tl l){
		uid, name, r: string;
		s := hd l;
		if(s == "" || s[0] == '#')
			continue;
		(uid, r) = field(s, ':');
		(name, r) = field(r, ':');
		if(uid == nil || name == nil || string int uid != uid){
			cprint("invalid /adm/users line: "+hd l);
			users = oldusers;
			return 0;
		}
		adduser(int uid, name, nil, nil);
	}

	# second pass: groups and leaders
	for(l = lines; l != nil; l = tl l){
		s := hd l;
		if(s == "" || s[0] == '#')
			continue;
		name, lead, mem, r: string;
		(nil, r) = field(s, ':');	# skip id
		(name, r) = field(r, ':');
		(lead, mem) = field(r, ':');
		(nil, mems) := sys->tokenize(mem, ",\n");
		if(name == nil || lead == nil && mems == nil)
			continue;
		u := finduname(name);
		if(lead != nil){
			lu := strtouid(lead);
			if(lu != None)
				u.leader = lu;
			else if(lead != nil)
				u.leader = u.uid;	# mimic kfs not fs
		}
		mids: list of int = nil;
		for(; mems != nil; mems = tl mems){
			lu := strtouid(hd mems);
			if(lu != None)
				mids = lu :: mids;
		}
		u.mem = mids;
	}

	if(debug)
	for(x := users; x != nil; x = tl x){
		u := hd x;
		sys->print("%d : %q : %d :", u.uid, u.name, u.leader);
		for(y := u.mem; y != nil; y = tl y)
			sys->print(" %d", hd y);
		sys->print("\n");
	}
	return 1;
}

field(s: string, c: int): (string, string)
{
	for(i := 0; i < len s; i++)
		if(s[i] == c)
			return (s[0:i], s[i+1:]);
	return (s, nil);
}

defaultusers()
{
	for(i := 0; i < len admusers; i++){
		(id, name, leader) := admusers[i];
		adduser(id, name, leader, nil);
	}
}

finduname(s: string): ref User
{
	for(l := users; l != nil; l = tl l){
		u := hd l;
		if(u.name == s)
			return u;
	}
	return nil;
}

uidtostr(id: int): string
{
	if(id == None)
		return "none";
	for(l := users; l != nil; l = tl l){
		u := hd l;
		if(u.uid == id)
			return u.name;
	}
	return sys->sprint("#%d", id);
}

leadgroup(ui: int, gi: int): int
{
	for(l := users; l != nil; l = tl l){
		u := hd l;
		if(u.uid == gi){
			if(u.leader == ui)
				return 1;
			if(u.leader == 0)
				return ingroup(ui, gi);
			return 0;
		}
	}
	return 0;
}

strtouid(s: string): int
{
	if(s == "none")
		return None;
	u := finduname(s);
	if(u != nil)
		return u.uid;
	return 0;
}

ingroup(uid: int, gid: int): int
{
	if(uid == gid)
		return 1;
	for(l := users; l != nil; l = tl l){
		u := hd l;
		if(u.uid == gid){
			for(m := u.mem; m != nil; m = tl m)
				if(hd m == uid)
					return 1;
			return 0;
		}
	}
	return 0;
}

baduname(s: string): int
{
	n := checkname9p2(s);
	if(n == 0 || n+1 > NAMELEN || s == "." || s == ".."){
		sys->print("kfs: illegal user name %q\n", s);
		return 1;
	}
	return 0;
}

adduser(id: int, name: string, leader: string, mem: list of string)
{
	if(baduname(name))
		return;
	for(l := users; l != nil; l = tl l){
		u := hd l;
		if(u.uid == id){
			sys->print("kfs: duplicate user ID %d (name %q)\n", id, u.name);
			return;
		}else if(u.name == name){
			sys->print("kfs: duplicate user name %q (id %d)\n", name, u.uid);
			return;
		}
	}
	if(name == leader)
		lid := id;
	else if(leader == nil)
		lid = 0;
	else if(!baduname(leader))
		lid = strtouid(leader);
	else
		return;
	memid: list of int;
	for(; mem != nil; mem = tl mem){
		if(baduname(hd mem))
			return;
		x := strtouid(hd mem);
		if(x != 0)
			memid = x :: memid;
	}
	u := ref User(id, name, lid, memid);
	users = u :: users;
}

Lock.new(): ref Lock
{
	return ref Lock(chan[1] of int);
}

Lock.lock(l: self ref Lock)
{
	l.c <-= 1;
}

Lock.canlock(l: self ref Lock): int
{
	alt{
	l.c <-= 1 =>
		return 1;
	* =>
		return 0;
	}
}

Lock.unlock(l: self ref Lock)
{
	<-l.c;
}

#
# kfs check, could be a separate module if that seemed important
#

MAXDEPTH: con 100;
MAXNAME: con 4000;

Map: adt {
	lo, hi:	int;
	bits:	array of byte;
	nbad:	int;
	ndup:	int;
	nmark:	int;

	new:	fn(lo, hi: int): ref Map;
	isset:	fn(b: self ref Map, a: int): int;
	mark:	fn(b: self ref Map, a: int): string;
};

Check: adt {
	dev:	ref Device;

	amap:	ref Map;
	qmap:	ref Map;

	name:	string;
	nfiles:	int;
	maxq:	int;

	mod:	int;
	flags:	int;
	oldblock:	int;

	depth:	int;
	maxdepth:	int;

	check:	fn(c: self ref Check);
	touch:	fn(c: self ref Check, a: int): int;
	checkdir:	fn(c: self ref Check, a: int, qpath: int): int;
	checkindir:	fn(c: self ref Check, a: int, d: ref Dentry, qpath: int): int;
	maked:	fn(c: self ref Check, a: int, s: int, qpath: int): ref Dentry;
	modd:	fn(c: self ref Check, a: int, s: int, d: ref Dentry);
	fsck:		fn(c: self ref Check, d: ref Dentry): int;
	xread:	fn(c: self ref Check, a: int, qpath: int);
	xtag:		fn(c: self ref Check, a: int, tag: int, qpath: int): ref Iobuf;
	ckfreelist:	fn(c: self ref Check, sb: ref Superb);
	mkfreelist:	fn(c: self ref Check, sb: ref Superb);
	amark:	fn(c: self ref Check, a: int): int;
	fmark:	fn(c: self ref Check, a: int): int;
	missing:	fn(c: self ref Check, sb: ref Superb);
	qmark:	fn(c: self ref Check, q: int);
};

check(dev: ref Device, flag: int)
{
	#mainlock.wlock();
	#mainlock.wunlock();
	c := ref Check;
	c.dev = dev;
	c.nfiles = 0;
	c.maxq = 0;
	c.mod = 0;
	c.flags = flag;
	c.oldblock = 0;
	c.depth = 0;
	c.maxdepth = 0;
	c.check();
}

checkflags(s: string): int
{
	f := 0;
	for(i := 0; i < len s; i++)
		case s[i] {
		'r' =>	f |= Crdall;
		't' => f |= Ctag;
		'P' => f |= Cpfile;
		'p' => f |= Cpdir;
		'f' => f |= Cfree;
		'c' => f |= Cream;
		'd' => f |= Cbad;
		'w' => f |= Ctouch;
		'q' => f |= Cquiet;
		'v' => ;	# old verbose flag; ignored
		* =>	return -1;
	}
	return f;
}

Check.check(c: self ref Check)
{
	sbaddr := SUPERADDR;
	p := c.xtag(sbaddr, Tsuper, QPSUPER);
	if(p == nil){
		cprint(sys->sprint("bad superblock"));
		return;
	}
	sb := Superb.unpack(p.iobuf);
	sb.iob = p;

	fstart := sb.fstart;
	if(fstart != 1){
		cprint(sys->sprint("invalid superblock"));
		return;
	}
	fsize := sb.fsize;
	if(fsize < fstart || fsize > wrensize(c.dev)){
		cprint(sys->sprint("invalid size in superblock"));
		return;
	}
	c.amap = Map.new(fstart, fsize);

	nqid := sb.qidgen+100;		# not as much of a botch
	if(nqid > 1024*1024*8)
		nqid = 1024*1024*8;
	if(nqid < 64*1024)
		nqid = 64*1024;
	c.qmap = Map.new(0, nqid);

	c.mod = 0;
	c.depth = 0;
	c.maxdepth = 0;

	if(c.amark(sbaddr))
		{}

	if(!(c.flags & Cquiet))
		cprint(sys->sprint("checking file system: %s", "main"));
	c.nfiles = 0;
	c.maxq = 0;

	d := c.maked(ROOTADDR, 0, QPROOT);
	if(d != nil){
		if(c.amark(ROOTADDR))
			{}
		if(c.fsck(d))
			c.modd(ROOTADDR, 0, d);
		if(--c.depth != 0)
			cprint("depth not zero on return");
	}
	if(sb.qidgen < c.maxq)
		cprint(sys->sprint("qid generator low path=%d maxq=%d", sb.qidgen, c.maxq));

	nqbad := c.qmap.nbad + c.qmap.ndup;
	c.qmap = nil;	# could use to implement resequence

	ndup := c.amap.ndup;
	nused := c.amap.nmark;

	c.amap.ndup = c.amap.nmark = 0;	# reset for free list counts
	if(c.flags & Cfree){
		c.name = "free list";
		c.mkfreelist(sb);
		sb.qidgen = c.maxq;
		p.settag(Tsuper, QPNONE);
	}else
		c.ckfreelist(sb);

	nbad := c.amap.nbad;
	nfdup := c.amap.ndup;
	nfree := c.amap.nmark;
	# leave amap for missing, below

	if(c.mod){
		cprint("file system was modified");
		p.settag(Tsuper, QPNONE);
	}

	if(!(c.flags & Cquiet)){
		cprint(sys->sprint("%8d files", c.nfiles));
		cprint(sys->sprint("%8d blocks in the file system", fsize-fstart));
		cprint(sys->sprint("%8d used blocks", nused));
		cprint(sys->sprint("%8d free blocks", sb.tfree));
	}
	if(!(c.flags & Cfree)){
		if(nfree != sb.tfree)
			cprint(sys->sprint("%8d free blocks found", nfree));
		if(nfdup)
			cprint(sys->sprint("%8d blocks duplicated in the free list", nfdup));
		if(fsize-fstart-nused-nfree)
			cprint(sys->sprint("%8d missing blocks", fsize-fstart-nused-nfree));
	}
	if(ndup)
		cprint(sys->sprint("%8d address duplications", ndup));
	if(nbad)
		cprint(sys->sprint("%8d bad block addresses", nbad));
	if(nqbad)
		cprint(sys->sprint("%8d bad qids", nqbad));
	if(!(c.flags & Cquiet))
		cprint(sys->sprint("%8d maximum qid path", c.maxq));
	c.missing(sb);

	sb.put();
}

Check.touch(c: self ref Check, a: int): int
{
	if((c.flags&Ctouch) && a){
		p := Iobuf.get(c.dev, a, Bread|Bmod);
		if(p != nil)
			p.put();
		return 1;
	}
	return 0;
}

Check.checkdir(c: self ref Check, a: int, qpath: int): int
{
	ns := len c.name;
	dmod := c.touch(a);
	for(i:=0; i<DIRPERBUF; i++){
		nd := c.maked(a, i, qpath);
		if(nd == nil)
			break;
		if(c.fsck(nd)){
			c.modd(a, i, nd);
			dmod++;
		}
		c.depth--;
		c.name = c.name[0:ns];
	}
	c.name = c.name[0:ns];
	return dmod;
}

Check.checkindir(c: self ref Check, a: int, d: ref Dentry, qpath: int): int
{
	dmod := c.touch(a);
	p := c.xtag(a, Tind1, qpath);
	if(p == nil)
		return dmod;
	for(i:=0; i<INDPERBUF; i++){
		a = get4(p.iobuf, i*4);
		if(a == 0)
			continue;
		if(c.amark(a)){
			if(c.flags & Cbad){
				put4(p.iobuf, i*4, 0);
				p.flags |= Bmod;
			}
			continue;
		}
		if(d.mode & DDIR)
			dmod += c.checkdir(a, qpath);
		else if(c.flags & Crdall)
			c.xread(a, qpath);
	}
	p.put();
	return dmod;
}

Check.fsck(c: self ref Check, d: ref Dentry): int
{
	p: ref Iobuf;
	i: int;
	a, qpath: int;

	if(++c.depth >= c.maxdepth){
		c.maxdepth = c.depth;
		if(c.maxdepth >= MAXDEPTH){
			cprint(sys->sprint("max depth exceeded: %s", c.name));
			return 0;
		}
	}
	dmod := 0;
	if(!(d.mode & DALLOC))
		return 0;
	c.nfiles++;

	ns := len c.name;
	i = styx->utflen(d.name);
	if(i >= NAMELEN){
		d.name[NAMELEN-1] = 0;	# TO DO: not quite right
		cprint(sys->sprint("%q.name (%q) not terminated", c.name, d.name));
		return 0;
	}
	ns += i;
	if(ns >= MAXNAME){
		cprint(sys->sprint("%q.name (%q) name too large", c.name, d.name));
		return 0;
	}
	c.name += d.name;

	if(d.mode & DDIR){
		if(ns > 1)
			c.name += "/";
		if(c.flags & Cpdir)
			cprint(sys->sprint("%s", c.name));
	} else if(c.flags & Cpfile)
		cprint(sys->sprint("%s", c.name));

	qpath = int d.qid.path & ~QPDIR;
	c.qmark(qpath);
	if(qpath > c.maxq)
		c.maxq = qpath;
	for(i=0; i<NDBLOCK; i++){
		a = get4(d.buf, Odblock+i*4);
		if(a == 0)
			continue;
		if(c.amark(a)){
			put4(d.buf, Odblock+i*4, 0);
			dmod++;
			continue;
		}
		if(d.mode & DDIR)
			dmod += c.checkdir(a, qpath);
		else if(c.flags & Crdall)
			c.xread(a, qpath);
	}
	a = get4(d.buf, Oiblock);
	if(a){
		if(c.amark(a)){
			put4(d.buf, Oiblock, 0);
			dmod++;
		}
		else
			dmod += c.checkindir(a, d, qpath);
	}

	a = get4(d.buf, Odiblock);
	if(a && c.amark(a)){
		put4(d.buf, Odiblock, 0);
		return dmod + 1;
	}
	dmod += c.touch(a);
	p = c.xtag(a, Tind2, qpath);
	if(p != nil){
		for(i=0; i<INDPERBUF; i++){
			a = get4(p.iobuf, i*4);
			if(a == 0)
				continue;
			if(c.amark(a)){
				if(c.flags & Cbad){
					put4(p.iobuf, i*4, 0);
					p.flags |= Bmod;
				}
				continue;
			}
			dmod += c.checkindir(a, d, qpath);
		}
		p.put();
	}
	return dmod;
}

Check.ckfreelist(c: self ref Check, sb: ref Superb)
{
	c.name = "free list";
	cprint(sys->sprint("check %s", c.name));
	fb := sb.fbuf;
	a := SUPERADDR;
	p: ref Iobuf;
	lo := 0;
	hi := 0;
	for(;;){
		n := get4(fb, 0);		# nfree
		if(n < 0 || n > FEPERBUF){
			cprint(sys->sprint("check: nfree bad %d", a));
			break;
		}
		for(i:=1; i<n; i++){
			a = get4(fb, 4+i*4);	# free[i]
			if(a && !c.fmark(a)){
				if(!lo || lo > a)
					lo = a;
				if(!hi || hi < a)
					hi = a;
			}
		}
		a = get4(fb, 4);	# free[0]
		if(a == 0)
			break;
		if(c.fmark(a))
			break;
		if(!lo || lo > a)
			lo = a;
		if(!hi || hi < a)
			hi = a;
		if(p != nil)
			p.put();
		p = c.xtag(a, Tfree, QPNONE);
		if(p == nil)
			break;
		fb = p.iobuf;
	}
	if(p != nil)
		p.put();
	cprint(sys->sprint("lo = %d; hi = %d", lo, hi));
}

#
# make freelist from scratch
#
Check.mkfreelist(c: self ref Check, sb: ref Superb)
{
	sb.fbuf[0:] = emptyblock[0:(FEPERBUF+1)*4];
	sb.tfree = 0;
	put4(sb.fbuf, 0, 1);	# nfree = 1
	for(a:=sb.fsize-sb.fstart-1; a >= 0; a--){
		i := a>>3;
		if(i < 0 || i >= len c.amap.bits)
			continue;
		b := byte (1 << (a&7));
		if((c.amap.bits[i] & b) != byte 0)
			continue;
		addfree(c.dev, sb.fstart+a, sb);
		c.amap.bits[i] |= b;
	}
	sb.iob.flags |= Bmod;
}

#
# makes a copy of a Dentry's representation on disc so that
# the rest of the much larger iobuf can be freed.
#
Check.maked(c: self ref Check, a: int, s: int, qpath: int): ref Dentry
{
	p := c.xtag(a, Tdir, qpath);
	if(p == nil)
		return nil;
	d := Dentry.get(p, s);
	if(d == nil)
		return nil;
	copy := array[len d.buf] of byte;
	copy[0:] = d.buf;
	d.put();
	d.buf = copy;
	return d;
}

Check.modd(c: self ref Check, a: int, s: int, d1: ref Dentry)
{
	if(!(c.flags & Cbad))
		return;
	p := Iobuf.get(c.dev, a, Bread);
	d := Dentry.get(p, s);
	if(d == nil){
		if(p != nil)
			p.put();
		return;
	}
	d.buf[0:] = d1.buf;
	p.flags |= Bmod;
	p.put();
}

Check.xread(c: self ref Check, a: int, qpath: int)
{
	p := c.xtag(a, Tfile, qpath);
	if(p != nil)
		p.put();
}

Check.xtag(c: self ref Check, a: int, tag: int, qpath: int): ref Iobuf
{
	if(a == 0)
		return nil;
	p := Iobuf.get(c.dev, a, Bread);
	if(p == nil){
		cprint(sys->sprint("check: \"%s\": xtag: p null", c.name));
		if(c.flags & (Cream|Ctag)){
			p = Iobuf.get(c.dev, a, Bmod);
			if(p != nil){
				p.iobuf[0:] = emptyblock;
				p.settag(tag, qpath);
				c.mod++;
				return p;
			}
		}
		return nil;
	}
	if(p.checktag(tag, qpath)){
		cprint(sys->sprint("check: \"%s\": xtag: checktag", c.name));
		if(c.flags & Cream)
			p.iobuf[0:] = emptyblock;
		if(c.flags & (Cream|Ctag)){
			p.settag(tag, qpath);
			c.mod++;
		}
		return p;
	}
	return p;
}

Check.amark(c: self ref Check, a: int): int
{
	e := c.amap.mark(a);
	if(e != nil){
		cprint(sys->sprint("check: \"%s\": %s %d", c.name, e, a));
		return e != "dup";	# don't clear dup blocks because rm might repair
	}
	return 0;
}

Check.fmark(c: self ref Check,a: int): int
{
	e := c.amap.mark(a);
	if(e != nil){
		cprint(sys->sprint("check: \"%s\": %s %d", c.name, e, a));
		return 1;
	}
	return 0;
}

Check.missing(c: self ref Check, sb: ref Superb)
{
	n := 0;
	for(a:=sb.fsize-sb.fstart-1; a>=0; a--){
		i := a>>3;
		b := byte (1 << (a&7));
		if((c.amap.bits[i] & b) == byte 0){
			cprint(sys->sprint("missing: %d", sb.fstart+a));
			n++;
		}
		if(n > 10){
			cprint(sys->sprint(" ..."));
			break;
		}
	}
}

Check.qmark(c: self ref Check, qpath: int)
{
	e := c.qmap.mark(qpath);
	if(e != nil){
		if(c.qmap.nbad+c.qmap.ndup < 20)
			cprint(sys->sprint("check: \"%s\": qid %s 0x%ux", c.name, e, qpath));
	}
}

Map.new(lo, hi: int): ref Map
{
	m := ref Map;
	n := (hi-lo+7)>>3;
	m.bits = array[n] of {* => byte 0};
	m.lo = lo;
	m.hi = hi;
	m.nbad = 0;
	m.ndup = 0;
	m.nmark = 0;
	return m;
}

Map.isset(m: self ref Map, i: int): int
{
	if(i < m.lo || i >= m.hi)
		return -1;	# hard to say
	i -= m.lo;
	return (m.bits[i>>3] & byte (1<<(i&7))) != byte 0;
}

Map.mark(m: self ref Map, i: int): string
{
	if(i < m.lo || i >= m.hi){
		m.nbad++;
		return "out of range";
	}
	i -= m.lo;
	b := byte (1 << (i&7));
	i >>= 3;
	if((m.bits[i] & b) != byte 0){
		m.ndup++;
		return "dup";
	}
	m.bits[i] |= b;
	m.nmark++;
	return nil;
}

cprint(s: string)
{
	if(consoleout != nil)
		consoleout <-= s+"\n";
	else
		eprint(s);
}