shithub: purgatorio

Download patch

ref: 1dbb193077af7ba6ff7fb70a4dd465480764382e
parent: 82a3f55c5fb7b9f7a82449e4eb943c535ec3e491
author: henesy <devnull@localhost>
date: Wed Mar 13 02:31:57 EDT 2019

add more missing files, some bad regex must have happened at some point

ape/diff: b/appl/cmd/disk/prep/null: No such file or directory ape/diff: b/appl/cmd/disk/null: No such file or directory ape/diff: b/keydb/null: No such file or directory
Binary files /dev/null and b/Linux/power/bin/limbo differ
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/asm/y.debug	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,230 @@
+yytoknames = array[] of {
+	"$end",
+	"error",
+	"$unk",
+	" |",
+	" ^",
+	" &",
+	" <",
+	" >",
+	" +",
+	" -",
+	" *",
+	" /",
+	" %",
+	"TOKI0",
+	"TOKI1",
+	"TOKI2",
+	"TOKI3",
+	"TCONST",
+	"TOKSB",
+	"TOKFP",
+	"TOKHEAP",
+	"TOKDB",
+	"TOKDW",
+	"TOKDL",
+	"TOKDF",
+	"TOKDS",
+	"TOKVAR",
+	"TOKEXT",
+	"TOKMOD",
+	"TOKLINK",
+	"TOKENTRY",
+	"TOKARRAY",
+	"TOKINDIR",
+	"TOKAPOP",
+	"TOKLDTS",
+	"TOKEXCS",
+	"TOKEXC",
+	"TOKETAB",
+	"TOKSRC",
+	"TID",
+	"TFCONST",
+	"TSTRING",
+	" :",
+	" ,",
+	" $",
+	" (",
+	" )",
+	" ~",
+};
+yystates = array [] of {
+	nil, #0
+	"$accept:  prog.$end \n", #1/
+	nil, #2
+	nil, #3
+	"label:  TID.: inst \n", #4/
+	"label:  TOKHEAP.heapid , expr ptrs \n", #5/
+	nil, #6
+	nil, #7
+	"data:  TOKDB.expr , elist \n", #8/
+	"data:  TOKDW.expr , elist \n", #9/
+	"data:  TOKDL.expr , elist \n", #10/
+	"data:  TOKDF.expr , TCONST \ndata:  TOKDF.expr , TFCONST \ndata:  TOKDF.expr , TID \ndata:  TOKDF.expr , - TCONST \ndata:  TOKDF.expr , - TFCONST \ndata:  TOKDF.expr , - TID \n", #11/
+	"data:  TOKDS.expr , TSTRING \n", #12/
+	"data:  TOKVAR.TID , expr \n", #13/
+	"data:  TOKEXT.expr , expr , TSTRING \n", #14/
+	"data:  TOKLINK.expr , expr , expr , TSTRING \n", #15/
+	"data:  TOKMOD.TID \n", #16/
+	"data:  TOKENTRY.expr , expr \n", #17/
+	"data:  TOKARRAY.expr , heapid , expr \n", #18/
+	"data:  TOKINDIR.expr , expr \n", #19/
+	nil, #20
+	"data:  TOKLDTS.TID , expr \n", #21/
+	"data:  TOKEXCS.expr \n", #22/
+	"data:  TOKEXC.expr , expr , expr , expr , expr , expr \n", #23/
+	"data:  TOKETAB.TSTRING , expr \ndata:  TOKETAB.* , expr \n", #24/
+	"data:  TOKSRC.TSTRING \n", #25/
+	"inst:  TOKI3.addr , addr \ninst:  TOKI3.addr , raddr , addr \n", #26/
+	"inst:  TOKI2.addr , addr \n", #27/
+	"inst:  TOKI1.addr \n", #28/
+	nil, #29
+	"label:  TID :.inst \n", #30/
+	"label:  TOKHEAP heapid., expr ptrs \n", #31/
+	"heapid:  $.expr \n", #32/
+	nil, #33
+	"data:  TOKDB expr., elist \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #34/
+	nil, #35
+	nil, #36
+	nil, #37
+	"con:  -.con \n", #38/
+	"con:  +.con \n", #39/
+	"con:  ~.con \n", #40/
+	"con:  (.expr ) \n", #41/
+	"data:  TOKDW expr., elist \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #42/
+	"data:  TOKDL expr., elist \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #43/
+	"data:  TOKDF expr., TCONST \ndata:  TOKDF expr., TFCONST \ndata:  TOKDF expr., TID \ndata:  TOKDF expr., - TCONST \ndata:  TOKDF expr., - TFCONST \ndata:  TOKDF expr., - TID \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #44/
+	"data:  TOKDS expr., TSTRING \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #45/
+	"data:  TOKVAR TID., expr \n", #46/
+	"data:  TOKEXT expr., expr , TSTRING \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #47/
+	"data:  TOKLINK expr., expr , expr , TSTRING \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #48/
+	nil, #49
+	"data:  TOKENTRY expr., expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #50/
+	"data:  TOKARRAY expr., heapid , expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #51/
+	"data:  TOKINDIR expr., expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #52/
+	"data:  TOKLDTS TID., expr \n", #53/
+	nil, #54
+	"data:  TOKEXC expr., expr , expr , expr , expr , expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #55/
+	"data:  TOKETAB TSTRING., expr \n", #56/
+	"data:  TOKETAB *., expr \n", #57/
+	nil, #58
+	"inst:  TOKI3 addr., addr \ninst:  TOKI3 addr., raddr , addr \n", #59/
+	"addr:  $.expr \n", #60/
+	nil, #61
+	nil, #62
+	"mem:  *.roff \n", #63/
+	"mem:  expr.( roff ) \nroff:  expr.( TOKSB ) \nroff:  expr.( TOKFP ) \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #64/
+	nil, #65
+	"inst:  TOKI2 addr., addr \n", #66/
+	nil, #67
+	nil, #68
+	"label:  TOKHEAP heapid ,.expr ptrs \n", #69/
+	nil, #70
+	"data:  TOKDB expr ,.elist \n", #71/
+	"expr:  expr +.expr \n", #72/
+	"expr:  expr -.expr \n", #73/
+	"expr:  expr *.expr \n", #74/
+	"expr:  expr /.expr \n", #75/
+	"expr:  expr %.expr \n", #76/
+	"expr:  expr <.< expr \n", #77/
+	"expr:  expr >.> expr \n", #78/
+	"expr:  expr &.expr \n", #79/
+	"expr:  expr ^.expr \n", #80/
+	"expr:  expr |.expr \n", #81/
+	nil, #82
+	nil, #83
+	nil, #84
+	"con:  ( expr.) \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #85/
+	"data:  TOKDW expr ,.elist \n", #86/
+	"data:  TOKDL expr ,.elist \n", #87/
+	"data:  TOKDF expr ,.TCONST \ndata:  TOKDF expr ,.TFCONST \ndata:  TOKDF expr ,.TID \ndata:  TOKDF expr ,.- TCONST \ndata:  TOKDF expr ,.- TFCONST \ndata:  TOKDF expr ,.- TID \n", #88/
+	"data:  TOKDS expr ,.TSTRING \n", #89/
+	"data:  TOKVAR TID ,.expr \n", #90/
+	"data:  TOKEXT expr ,.expr , TSTRING \n", #91/
+	"data:  TOKLINK expr ,.expr , expr , TSTRING \n", #92/
+	"data:  TOKENTRY expr ,.expr \n", #93/
+	"data:  TOKARRAY expr ,.heapid , expr \n", #94/
+	"data:  TOKINDIR expr ,.expr \n", #95/
+	"data:  TOKLDTS TID ,.expr \n", #96/
+	"data:  TOKEXC expr ,.expr , expr , expr , expr , expr \n", #97/
+	"data:  TOKETAB TSTRING ,.expr \n", #98/
+	"data:  TOKETAB * ,.expr \n", #99/
+	"inst:  TOKI3 addr ,.addr \ninst:  TOKI3 addr ,.raddr , addr \n", #100/
+	nil, #101
+	nil, #102
+	"roff:  expr.( TOKSB ) \nroff:  expr.( TOKFP ) \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #103/
+	"mem:  expr (.roff ) \nroff:  expr (.TOKSB ) \nroff:  expr (.TOKFP ) \n", #104/
+	"inst:  TOKI2 addr ,.addr \n", #105/
+	nil, #106
+	nil, #107
+	nil, #108
+	nil, #109
+	nil, #110
+	nil, #111
+	nil, #112
+	nil, #113
+	"expr:  expr < <.expr \n", #114/
+	"expr:  expr > >.expr \n", #115/
+	nil, #116
+	nil, #117
+	nil, #118
+	nil, #119
+	nil, #120
+	nil, #121
+	nil, #122
+	nil, #123
+	nil, #124
+	"data:  TOKDF expr , -.TCONST \ndata:  TOKDF expr , -.TFCONST \ndata:  TOKDF expr , -.TID \n", #125/
+	nil, #126
+	nil, #127
+	"data:  TOKEXT expr , expr., TSTRING \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #128/
+	"data:  TOKLINK expr , expr., expr , TSTRING \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #129/
+	nil, #130
+	"data:  TOKARRAY expr , heapid., expr \n", #131/
+	nil, #132
+	nil, #133
+	"data:  TOKEXC expr , expr., expr , expr , expr , expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #134/
+	nil, #135
+	nil, #136
+	nil, #137
+	"inst:  TOKI3 addr , raddr., addr \n", #138/
+	"raddr:  $.expr \naddr:  $.expr \n", #139/
+	nil, #140
+	"roff:  expr (.TOKSB ) \nroff:  expr (.TOKFP ) \n", #141/
+	"mem:  expr ( roff.) \n", #142/
+	"roff:  expr ( TOKSB.) \n", #143/
+	"roff:  expr ( TOKFP.) \n", #144/
+	nil, #145
+	nil, #146
+	"ptrs:  ,.TSTRING \n", #147/
+	"elist:  elist ,.expr \n", #148/
+	nil, #149
+	nil, #150
+	nil, #151
+	nil, #152
+	nil, #153
+	"data:  TOKEXT expr , expr ,.TSTRING \n", #154/
+	"data:  TOKLINK expr , expr ,.expr , TSTRING \n", #155/
+	"data:  TOKARRAY expr , heapid ,.expr \n", #156/
+	"data:  TOKEXC expr , expr ,.expr , expr , expr , expr \n", #157/
+	"inst:  TOKI3 addr , raddr ,.addr \n", #158/
+	nil, #159
+	nil, #160
+	nil, #161
+	nil, #162
+	nil, #163
+	nil, #164
+	nil, #165
+	"data:  TOKLINK expr , expr , expr., TSTRING \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #166/
+	nil, #167
+	"data:  TOKEXC expr , expr , expr., expr , expr , expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #168/
+	nil, #169
+	"data:  TOKLINK expr , expr , expr ,.TSTRING \n", #170/
+	"data:  TOKEXC expr , expr , expr ,.expr , expr , expr \n", #171/
+	nil, #172
+	"data:  TOKEXC expr , expr , expr , expr., expr , expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #173/
+	"data:  TOKEXC expr , expr , expr , expr ,.expr , expr \n", #174/
+	"data:  TOKEXC expr , expr , expr , expr , expr., expr \nexpr:  expr.+ expr \nexpr:  expr.- expr \nexpr:  expr.* expr \nexpr:  expr./ expr \nexpr:  expr.% expr \nexpr:  expr.< < expr \nexpr:  expr.> > expr \nexpr:  expr.& expr \nexpr:  expr.^ expr \nexpr:  expr.| expr \n", #175/
+	"data:  TOKEXC expr , expr , expr , expr , expr ,.expr \n", #176/
+	nil, #177
+};
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/format.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,755 @@
+implement Format;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "disks.m";
+	disks: Disks;
+	Disk: import disks;
+
+include "arg.m";
+
+Format: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+#
+#  floppy types (all MFM encoding)
+#
+Type: adt {
+	name:	string;
+	bytes:	int;	# bytes/sector
+	sectors:	int;	# sectors/track
+	heads:	int;	# number of heads
+	tracks:	int;	# tracks/disk
+	media:	int;	# media descriptor byte
+	cluster:	int;	# default cluster size
+};
+
+floppytype := array[] of  {
+	Type ( "3½HD",	512, 18,	2,	80,	16rf0,	1 ),
+	Type ( "3½DD",	512,	  9,	2,	80,	16rf9,	2 ),
+	Type ( "3½QD",	512, 36,	2,	80,	16rf9,	2 ),	# invented
+	Type ( "5¼HD",	512,	15,	2,	80,	16rf9,	1 ),
+	Type ( "5¼DD",	512,	  9,	2,	40,	16rfd,	2 ),
+	Type	( "hard",	512,	  0,	0,	  0,	16rf8,	4 ),
+};
+
+# offsets in DOS boot area
+DB_MAGIC 	: con 0;
+DB_VERSION	: con 3;
+DB_SECTSIZE	: con 11;
+DB_CLUSTSIZE	: con 13;
+DB_NRESRV	: con 14;
+DB_NFATS	: con 16;
+DB_ROOTSIZE	: con	17;
+DB_VOLSIZE	: con	19;
+DB_MEDIADESC: con 21;
+DB_FATSIZE	: con 22;
+DB_TRKSIZE	: con 24;
+DB_NHEADS	: con 26;
+DB_NHIDDEN	: con 28;
+DB_BIGVOLSIZE: con 32;
+DB_DRIVENO 	: con 36;
+DB_RESERVED0: con 37;
+DB_BOOTSIG	: con 38;
+DB_VOLID	: con 39;
+DB_LABEL	: con 43;
+DB_TYPE		: con 54;
+
+DB_VERSIONSIZE: con 8;
+DB_LABELSIZE	: con 11;
+DB_TYPESIZE	: con 8;
+DB_SIZE		: con 62;
+
+# offsets in DOS directory
+DD_NAME	: con 0;
+DD_EXT		: con 8;
+DD_ATTR		: con 11;
+DD_RESERVED 	: con 12;
+DD_TIME		: con 22;
+DD_DATE		: con 24;
+DD_START	: con 26;
+DD_LENGTH	: con 28;
+
+DD_NAMESIZE	: con 8;
+DD_EXTSIZE	: con 3;
+DD_SIZE		: con 32;
+
+DRONLY	: con 16r01;
+DHIDDEN	: con 16r02;
+DSYSTEM	: con byte 16r04;
+DVLABEL	: con byte 16r08;
+DDIR	: con byte 16r10;
+DARCH	: con byte 16r20;
+
+#  the boot program for the boot sector.
+bootprog := array[512] of {
+16r000 =>
+	byte 16rEB, byte 16r3C, byte 16r90, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00,
+	byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00,
+16r03E =>
+	byte 16rFA, byte 16rFC, byte 16r8C, byte 16rC8, byte 16r8E, byte 16rD8, byte 16r8E, byte 16rD0,
+	byte 16rBC, byte 16r00, byte 16r7C, byte 16rBE, byte 16r77, byte 16r7C, byte 16rE8, byte 16r19,
+	byte 16r00, byte 16r33, byte 16rC0, byte 16rCD, byte 16r16, byte 16rBB, byte 16r40, byte 16r00,
+	byte 16r8E, byte 16rC3, byte 16rBB, byte 16r72, byte 16r00, byte 16rB8, byte 16r34, byte 16r12,
+	byte 16r26, byte 16r89, byte 16r07, byte 16rEA, byte 16r00, byte 16r00, byte 16rFF, byte 16rFF,
+	byte 16rEB, byte 16rD6, byte 16rAC, byte 16r0A, byte 16rC0, byte 16r74, byte 16r09, byte 16rB4,
+	byte 16r0E, byte 16rBB, byte 16r07, byte 16r00, byte 16rCD, byte 16r10, byte 16rEB, byte 16rF2,
+	byte 16rC3,  byte 'N',  byte 'o',  byte 't',  byte ' ',  byte 'a',  byte ' ',  byte 'b',
+	byte 'o',  byte 'o',  byte 't',  byte 'a',  byte 'b',  byte 'l',  byte 'e',  byte ' ',
+	byte 'd',  byte 'i',  byte 's',  byte 'c',  byte ' ',  byte 'o',  byte 'r',  byte ' ',
+	byte 'd',  byte 'i',  byte 's',  byte 'c',  byte ' ',  byte 'e',  byte 'r',  byte 'r',
+	byte 'o',  byte 'r', byte '\r', byte '\n',  byte 'P',  byte 'r',  byte 'e',  byte 's',
+	byte 's',  byte ' ',  byte 'a',  byte 'l',  byte 'm',  byte 'o',  byte 's',  byte 't',
+	byte ' ',  byte 'a',  byte 'n',  byte 'y',  byte ' ',  byte 'k',  byte 'e',  byte 'y',
+	byte ' ',  byte 't',  byte 'o',  byte ' ',  byte 'r',  byte 'e',  byte 'b',  byte 'o',
+	byte 'o',  byte 't',  byte '.',  byte '.',  byte '.', byte 16r00, byte 16r00, byte 16r00,
+16r1F0 =>
+	byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00,
+	byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r55, byte 16rAA,
+* =>
+	byte 16r00,
+};
+
+dev: string;
+clustersize := 0;
+fat: array of byte;	# the fat
+fatbits: int;
+fatsecs: int;
+fatlast: int;	# last cluster allocated
+clusters: int;
+volsecs: int;
+root: array of byte;	# first block of root
+rootsecs: int;
+rootfiles: int;
+rootnext: int;
+chatty := 0;
+xflag := 0;
+nresrv := 1;
+dos := 0;
+fflag := 0;
+file: string;	# output file name
+pbs: string;
+typ: string;
+
+Sof: con 1;	# start of file
+Eof: con 2;	# end of file
+
+stdin, stdout, stderr: ref Sys->FD;
+
+fatal(str: string)
+{
+	sys->fprint(stderr, "format: %s\n", str);
+	if(fflag && file != nil)
+		sys->remove(file);
+	raise "fail:error";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	daytime = load Daytime Daytime->PATH;
+	disks = load Disks Disks->PATH;
+	arg := load Arg Arg->PATH;
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	disks->init();
+
+	fflag = 0;
+	typ = nil;
+	clustersize = 0;
+	writepbs := 0;
+	label := array[DB_LABELSIZE] of {* => byte ' '};
+	label[0:] = array of byte "CYLINDRICAL";
+	arg->init(args);
+	arg->setusage("disk/format [-df] [-b bootblock] [-c csize] [-l label] [-r nresrv] [-t type] disk [files ...]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'b' =>
+			pbs = arg->earg();
+			writepbs = 1;
+		'd' =>
+			dos = 1;
+			writepbs = 1;
+		'c' =>
+			clustersize = int arg->earg();
+		'f' =>
+			fflag = 1;
+		'l' =>
+			a := array of byte arg->earg();
+			if(len a > len label)
+				a = a[0:len label];
+			label[0:] = a;
+			for(i := len a; i < len label; i++)
+				label[i] = byte ' ';
+		'r' =>
+			nresrv = int arg->earg();
+		't' =>
+			typ = arg->earg();
+		'v' =>
+			chatty = 1;
+		'x' =>
+			xflag = 1;
+		* =>
+			arg->usage();
+	}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	dev = hd args;
+	disk := Disk.open(dev, Sys->ORDWR, 0);
+	if(disk == nil){
+		if(fflag){
+			fd := sys->create(dev, Sys->ORDWR, 8r666);
+			if(fd != nil){
+				fd = nil;
+				disk = Disk.open(dev, Sys->ORDWR, 0);
+			}
+		}
+		if(disk == nil)
+			fatal(sys->sprint("opendisk %q: %r", dev));
+	}
+
+	if(disk.dtype == "file")
+		fflag = 1;
+
+	if(typ == nil){
+		case disk.dtype {
+		"file" =>
+			typ = "3½HD";
+		"floppy" =>
+			sys->seek(disk.ctlfd, big 0, 0);
+			buf := array[10] of byte;
+			n := sys->read(disk.ctlfd, buf, len buf);
+			if(n <= 0 || n >= 10)
+				fatal("reading floppy type");
+			typ = string buf[0:n];
+		"sd" =>
+			typ = "hard";
+		* =>
+			typ = "unknown";
+		}
+	}
+
+	if(!fflag && disk.dtype == "floppy")
+		if(sys->fprint(disk.ctlfd, "format %s", typ) < 0)
+			fatal(sys->sprint("formatting floppy as %s: %r", typ));
+
+	if(disk.dtype != "floppy" && !xflag)
+		sanitycheck(disk);
+
+	# check that everything will succeed
+	dosfs(dos, writepbs, disk, label, tl args, 0);
+
+	# commit
+	dosfs(dos, writepbs, disk, label, tl args, 1);
+
+	sys->print("used %bd bytes\n", big fatlast*big clustersize*big disk.secsize);
+	exit;
+}
+
+#
+# look for a partition table on sector 1, as would be the
+# case if we were erroneously formatting 9fat without -r 2.
+# if it's there and nresrv is not big enough, complain and exit.
+# i've blown away my partition table too many times.
+#
+sanitycheck(disk: ref Disk)
+{
+	buf := array[512] of byte;
+	bad := 0;
+	if(dos && nresrv < 2 && sys->seek(disk.fd, big disk.secsize, 0) == big disk.secsize &&
+	    sys->read(disk.fd, buf, len buf) >= 5 && string buf[0:5] == "part "){
+		sys->fprint(sys->fildes(2), "there's a plan9 partition on the disk\n"+
+			"and you didn't specify -r 2 (or greater).\n" +
+			"either specify -r 2 or -x to disable this check.\n");
+		bad = 1;
+	}
+
+	if(disk.dtype == "sd" && disk.offset == big 0){
+		sys->fprint(sys->fildes(2), "you're attempting to format your disk (/dev/sdXX/data)\n"+
+			"rather than a partition such as /dev/sdXX/9fat;\n" +
+			"this is probably a mistake.  specify -x to disable this check.\n");
+		bad = 1;
+	}
+
+	if(bad)
+		raise "fail:failed disk sanity check";
+}
+
+#
+# return the BIOS driver number for the disk.
+# 16r80 is the first fixed disk, 16r81 the next, etc.
+# We map sdC0=16r80, sdC1=16r81, sdD0=16r82, sdD1=16r83
+#
+getdriveno(disk: ref Disk): int
+{
+	if(disk.dtype != "sd")
+		return 16r80;	# first hard disk
+
+	name := sys->fd2path(disk.fd);
+	if(len name < 3)
+		return 16r80;
+
+	#
+	# The name is of the format #SsdC0/foo 
+	# or /dev/sdC0/foo.
+	# So that we can just look for /sdC0, turn 
+	# #SsdC0/foo into #/sdC0/foo.
+	#
+	if(name[0:1] == "#S")
+		name[1] = '/';
+
+	for(p := name; len p >= 4; p = p[1:])
+		if(p[0:2] == "sd" && (p[2]=='C' || p[2]=='D') && (p[3]=='0' || p[3]=='1'))
+			return 16r80 + (p[2]-'c')*2 + (p[3]-'0');
+
+	return 16r80;
+}
+
+writen(fd: ref Sys->FD, buf: array of byte, n: int): int
+{
+	# write 8k at a time, to be nice to the disk subsystem
+	m: int;
+	for(tot:=0; tot<n; tot+=m){
+		m = n - tot;
+		if(m > 8192)
+			m = 8192;
+		if(sys->write(fd, buf[tot:], m) != m)
+			break;
+	}
+	return tot;
+}
+
+dosfs(dofat: int, dopbs: int, disk: ref Disk, label: array of byte, arg: list of string, commit: int)
+{
+	if(dofat == 0 && dopbs == 0)
+		return;
+
+	for(i := 0; i < len floppytype; i++)
+		if(typ == floppytype[i].name)
+			break;
+	if(i == len floppytype)
+		fatal(sys->sprint("unknown floppy type %q", typ));
+
+	t := floppytype[i];
+	if(t.sectors == 0 && typ == "hard"){
+		t.sectors = disk.s;
+		t.heads = disk.h;
+		t.tracks = disk.c;
+	}
+
+	if(t.sectors == 0 && dofat)
+		fatal(sys->sprint("cannot format fat with type %s: geometry unknown", typ));
+
+	if(fflag){
+		disk.size = big (t.bytes*t.sectors*t.heads*t.tracks);
+		disk.secsize = t.bytes;
+		disk.secs = disk.size / big disk.secsize;
+	}
+
+	secsize := disk.secsize;
+	length := disk.size;
+
+	#
+	# make disk full size if a file
+	#
+	if(fflag && disk.dtype == "file"){
+		(ok, d) := sys->fstat(disk.wfd);
+		if(ok < 0)
+			fatal(sys->sprint("fstat disk: %r"));
+		if(commit && d.length < disk.size){
+			if(sys->seek(disk.wfd, disk.size-big 1, 0) < big 0)
+				fatal(sys->sprint("seek to 9: %r"));
+			if(sys->write(disk.wfd, array[] of {0 => byte '9'}, 1) < 0)
+				fatal(sys->sprint("writing 9: @%bd %r", sys->seek(disk.wfd, big 0, 1)));
+		}
+	}
+
+	buf := array[secsize] of byte;
+
+	#
+	# start with initial sector from disk
+	#
+	if(sys->seek(disk.fd, big 0, 0) < big 0)
+		fatal(sys->sprint("seek to boot sector: %r"));
+	if(commit && sys->read(disk.fd, buf, secsize) != secsize)
+		fatal(sys->sprint("reading boot sector: %r"));
+
+	if(dofat)
+		memset(buf, 0, DB_SIZE);
+
+	#
+	# Jump instruction and OEM name
+	#
+	b := buf;	# hmm.
+	b[DB_MAGIC+0] = byte 16rEB;
+	b[DB_MAGIC+1] = byte 16r3C;
+	b[DB_MAGIC+2] = byte 16r90;
+	memmove(b[DB_VERSION: ], array of byte "Plan9.00", DB_VERSIONSIZE);
+
+	#
+	# Add bootstrapping code; assume it starts
+	# at 16r3E (the destination of the jump we just
+	# wrote to b[DB_MAGIC]
+	#
+	if(dopbs){
+		pbsbuf := array[secsize] of byte;
+		npbs: int;
+		if(pbs != nil){
+			if((sysfd := sys->open(pbs, Sys->OREAD)) == nil)
+				fatal(sys->sprint("open %s: %r", pbs));
+			npbs = sys->read(sysfd, pbsbuf, len pbsbuf);
+			if(npbs < 0)
+				fatal(sys->sprint("read %s: %r", pbs));
+			if(npbs > secsize-2)
+				fatal("boot block too large");
+		}else{
+			pbsbuf[0:] = bootprog;
+			npbs = len bootprog;
+		}
+		if(npbs <= 16r3E)
+			sys->fprint(sys->fildes(2), "warning: pbs too small\n");
+		else
+			buf[16r3E:] = pbsbuf[16r3E:npbs];
+	}
+
+	#
+	# Add FAT BIOS parameter block
+	#
+	if(dofat){
+		if(commit){
+			sys->print("Initializing FAT file system\n");
+			sys->print("type %s, %d tracks, %d heads, %d sectors/track, %d bytes/sec\n",
+					t.name, t.tracks, t.heads, t.sectors, secsize);
+		}
+
+ 		if(clustersize == 0)
+	 		clustersize = t.cluster;
+		#
+		# the number of fat bits depends on how much disk is left
+		# over after you subtract out the space taken up by the fat tables.
+		# try both.  what a crock.
+		#
+		for(fatbits = 12;;){
+	 		volsecs = int (length/big secsize);
+			#
+			# here's a crock inside a crock.  even having fixed fatbits,
+			# the number of fat sectors depends on the number of clusters,
+			# but of course we don't know yet.  maybe iterating will get us there.
+			# or maybe it will cycle.
+			#
+			clusters = 0;
+			for(i=0;; i++){
+			 	fatsecs = (fatbits*clusters + 8*secsize - 1)/(8*secsize);
+			 	rootsecs = volsecs/200;
+			 	rootfiles = rootsecs * (secsize/DD_SIZE);
+				if(rootfiles > 512){
+					rootfiles = 512;
+					rootsecs = rootfiles/(secsize/DD_SIZE);
+				}
+				data := nresrv + 2*fatsecs + (rootfiles*DD_SIZE + secsize-1)/secsize;
+				newclusters := 2 + (volsecs - data)/clustersize;
+				if(newclusters == clusters)
+					break;
+				clusters = newclusters;
+				if(i > 10)
+					fatal(sys->sprint("can't decide how many clusters to use (%d? %d?)", clusters, newclusters));
+if(chatty) sys->print("clusters %d\n", clusters);
+if(clusters <= 1) raise "trap";
+			}
+
+if(chatty) sys->print("try %d fatbits => %d clusters of %d\n", fatbits, clusters, clustersize);
+			if(clusters < 4087 || fatbits > 12)
+				break;
+			fatbits = 16;
+		}
+		if(clusters >= 65527)
+			fatal("disk too big; implement fat32");
+
+		putshort(b[DB_SECTSIZE: ], secsize);
+		b[DB_CLUSTSIZE] = byte clustersize;
+		putshort(b[DB_NRESRV: ], nresrv);
+		b[DB_NFATS] = byte 2;
+		putshort(b[DB_ROOTSIZE: ], rootfiles);
+		if(volsecs < (1<<16))
+			putshort(b[DB_VOLSIZE: ], volsecs);
+		b[DB_MEDIADESC] = byte t.media;
+		putshort(b[DB_FATSIZE: ], fatsecs);
+		putshort(b[DB_TRKSIZE: ], t.sectors);
+		putshort(b[DB_NHEADS: ], t.heads);
+		putlong(b[DB_NHIDDEN: ], int disk.offset);
+		putlong(b[DB_BIGVOLSIZE: ], volsecs);
+
+		#
+		# Extended BIOS Parameter Block
+		#
+		if(t.media == 16rF8)
+			dno := getdriveno(disk);
+		else
+			dno = 0;
+if(chatty) sys->print("driveno = %ux\n", dno);
+		b[DB_DRIVENO] = byte dno;
+		b[DB_BOOTSIG] = byte 16r29;
+		x := int (disk.offset + big b[DB_NFATS]*big fatsecs + big nresrv);
+		putlong(b[DB_VOLID:], x);
+if(chatty) sys->print("volid = %ux\n", x);
+		b[DB_LABEL:] = label;
+		r := sys->aprint("FAT%d    ", fatbits);
+		if(len r > DB_TYPESIZE)
+			r = r[0:DB_TYPESIZE];
+		b[DB_TYPE:] = r;
+	}
+
+	b[secsize-2] = byte Disks->Magic0;
+	b[secsize-1] = byte Disks->Magic1;
+
+	if(commit){
+		if(sys->seek(disk.wfd, big 0, 0) < big 0)
+			fatal(sys->sprint("seek to boot sector: %r\n"));
+		if(sys->write(disk.wfd, b, secsize) != secsize)
+			fatal(sys->sprint("writing to boot sector: %r"));
+	}
+
+	#
+	# if we were only called to write the PBS, leave now
+	#
+	if(dofat == 0)
+		return;
+
+	#
+	#  allocate an in memory fat
+	#
+	if(sys->seek(disk.wfd, big (nresrv*secsize), 0) < big 0)
+		fatal(sys->sprint("seek to fat: %r"));
+if(chatty) sys->print("fat @%buX\n", sys->seek(disk.wfd, big 0, 1));
+	fat = array[fatsecs*secsize] of {* => byte 0};
+	if(fat == nil)
+		fatal("out of memory");
+	fat[0] = byte t.media;
+	fat[1] = byte 16rff;
+	fat[2] = byte 16rff;
+	if(fatbits == 16)
+		fat[3] = byte 16rff;
+	fatlast = 1;
+	if(sys->seek(disk.wfd, big (2*fatsecs*secsize), 1) < big 0)	# 2 fats
+		fatal(sys->sprint("seek to root: %r"));
+if(chatty) sys->print("root @%buX\n", sys->seek(disk.wfd, big 0, 1));
+
+	#
+	#  allocate an in memory root
+	#
+	root = array[rootsecs*secsize] of {* => byte 0};
+	if(sys->seek(disk.wfd, big (rootsecs*secsize), 1) < big 0)		# rootsecs
+		fatal(sys->sprint("seek to files: %r"));
+if(chatty) sys->print("files @%buX\n", sys->seek(disk.wfd, big 0, 1));
+
+	#
+	# Now positioned at the Files Area.
+	# If we have any arguments, process 
+	# them and write out.
+	#
+	for(p := 0; arg != nil; arg = tl arg){
+		if(p >= rootsecs*secsize)
+			fatal("too many files in root");
+		#
+		# Open the file and get its length.
+		#
+		if((sysfd := sys->open(hd arg, Sys->OREAD)) == nil)
+			fatal(sys->sprint("open %s: %r", hd arg));
+		(ok, d) := sys->fstat(sysfd);
+		if(ok < 0)
+			fatal(sys->sprint("stat %s: %r", hd arg));
+		if(d.length >= big 16r7FFFFFFF)
+			fatal(sys->sprint("file %s too big (%bd bytes)", hd arg, d.length));
+		if(commit)
+			sys->print("Adding file %s, length %bd\n", hd arg, d.length);
+
+		x: int;
+		length = d.length;
+		if(length > big 0){
+			#
+			# Allocate a buffer to read the entire file into.
+			# This must be rounded up to a cluster boundary.
+			#
+			# Read the file and write it out to the Files Area.
+			#
+			length += big (secsize*clustersize - 1);
+			length /= big (secsize*clustersize);
+			length *= big (secsize*clustersize);
+			fbuf := array[int length] of byte;
+			if((nr := sys->read(sysfd, fbuf, int d.length)) != int d.length){
+				if(nr >= 0)
+					sys->werrstr("short read");
+				fatal(sys->sprint("read %s: %r", hd arg));
+			}
+			for(; nr < len fbuf; nr++)
+				fbuf[nr] = byte 0;
+if(chatty) sys->print("%q @%buX\n", d.name, sys->seek(disk.wfd, big 0, 1));
+			if(commit && writen(disk.wfd, fbuf, len fbuf) != len fbuf)
+				fatal(sys->sprint("write %s: %r", hd arg));
+			fbuf = nil;
+
+			#
+			# Allocate the FAT clusters.
+			# We're assuming here that where we
+			# wrote the file is in sync with
+			# the cluster allocation.
+			# Save the starting cluster.
+			#
+			length /= big (secsize*clustersize);
+			x = clustalloc(Sof);
+			for(n := 0; n < int length-1; n++)
+				clustalloc(0);
+			clustalloc(Eof);
+		}
+		else
+			x = 0;
+
+		#
+		# Add the filename to the root.
+		#
+sys->fprint(sys->fildes(2), "add %s at clust %ux\n", d.name, x);
+		addrname(root[p:], d, hd arg, x);
+		p += DD_SIZE;
+	}
+
+	#
+	#  write the fats and root
+	#
+	if(commit){
+		if(sys->seek(disk.wfd, big (nresrv*secsize), 0) < big 0)
+			fatal(sys->sprint("seek to fat #1: %r"));
+		if(sys->write(disk.wfd, fat, fatsecs*secsize) < 0)
+			fatal(sys->sprint("writing fat #1: %r"));
+		if(sys->write(disk.wfd, fat, fatsecs*secsize) < 0)
+			fatal(sys->sprint("writing fat #2: %r"));
+		if(sys->write(disk.wfd, root, rootsecs*secsize) < 0)
+			fatal(sys->sprint("writing root: %r"));
+	}
+}
+
+#
+#  allocate a cluster
+#
+clustalloc(flag: int): int
+{
+	o, x: int;
+
+	if(flag != Sof){
+		if (flag == Eof)
+			x =16rffff;
+		else
+			x = fatlast+1;
+		if(fatbits == 12){
+			x &= 16rfff;
+			o = (3*fatlast)/2;
+			if(fatlast & 1){
+				fat[o] = byte ((int fat[o] & 16r0f) | (x<<4));
+				fat[o+1] = byte (x>>4);
+			} else {
+				fat[o] = byte x;
+				fat[o+1] = byte ((int fat[o+1] & 16rf0) | ((x>>8) & 16r0F));
+			}
+		} else {
+			o = 2*fatlast;
+			fat[o] = byte x;
+			fat[o+1] = byte (x>>8);
+		}
+	}
+		
+	if(flag == Eof)
+		return 0;
+	if(++fatlast >= clusters)
+		fatal(sys->sprint("data does not fit on disk (%d %d)", fatlast, clusters));
+	return fatlast;
+}
+
+putname(p: string, buf: array of byte)
+{
+	memset(buf[DD_NAME: ], ' ', DD_NAMESIZE+DD_EXTSIZE);
+	for(i := 0; i < DD_NAMESIZE && i < len p && p[i] != '.'; i++){
+		c := p[i];
+		if(c >= 'a' && c <= 'z')
+			c += 'A'-'a';
+		buf[DD_NAME+i] = byte c;
+	}
+	for(i = 0; i < len p; i++)
+		if(p[i] == '.'){
+			p = p[i+1:];
+			for(i = 0; i < DD_EXTSIZE && i < len p; i++){
+				c := p[i];
+				if(c >= 'a' && c <= 'z')
+					c += 'A'-'a';
+				buf[DD_EXT+i] = byte c;
+			}
+			break;
+		}
+}
+
+puttime(buf: array of byte)
+{
+	t := daytime->local(daytime->now());
+	x := (t.hour<<11) | (t.min<<5) | (t.sec>>1);
+	buf[DD_TIME+0] = byte x;
+	buf[DD_TIME+1] = byte (x>>8);
+	x = ((t.year-80)<<9) | ((t.mon+1)<<5) | t.mday;
+	buf[DD_DATE+0] = byte x;
+	buf[DD_DATE+1] = byte (x>>8);
+}
+
+addrname(buf: array of byte, dir: Sys->Dir, name: string, start: int)
+{
+	s := name;
+	for(i := len s; --i >= 0;)
+		if(s[i] == '/'){
+			s = s[i+1:];
+			break;
+		}
+	putname(s, buf);
+	if(s == "9load")
+		buf[DD_ATTR] = byte DSYSTEM;
+	else
+		buf[DD_ATTR] = byte 0;
+	puttime(buf);
+	buf[DD_START+0] = byte start;
+	buf[DD_START+1] = byte (start>>8);
+	buf[DD_LENGTH+0] = byte dir.length;
+	buf[DD_LENGTH+1] = byte (dir.length>>8);
+	buf[DD_LENGTH+2] = byte (dir.length>>16);
+	buf[DD_LENGTH+3] = byte (dir.length>>24);
+}
+
+memset(d: array of byte, v: int, n: int)
+{
+	for (i := 0; i < n; i++)
+		d[i] = byte v;
+}
+
+memmove(d: array of byte, s: array of byte, n: int)
+{
+	d[0:] = s[0:n];
+}
+
+putshort(b: array of byte, v: int)
+{
+	b[1] = byte (v>>8);
+	b[0] = byte v;
+}
+
+putlong(b: array of byte, v: int)
+{
+	putshort(b, v);
+	putshort(b[2: ], v>>16);
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/ftl.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,911 @@
+#
+# basic Flash Translation Layer driver
+#	see for instance the Intel technical paper
+#	``Understanding the Flash Translation Layer (FTL) Specification''
+#	Order number 297816-001 (online at www.intel.com)
+#
+# a public driver by David Hinds, dhinds@allegro.stanford.edu
+# further helps with some details.
+#
+# this driver uses the common simplification of never storing
+# the VBM on the medium (a waste of precious flash!) but
+# rather building it on the fly as the block maps are read.
+#
+# Plan 9 driver (c) 1997 by C H Forsyth (forsyth@caldo.demon.co.uk)
+#	This driver may be used or adapted by anyone for any non-commercial purpose.
+#
+# adapted for Inferno 1998 by C H Forsyth, Vita Nuova Limited, York, England (byteles@vitanuova.com)
+#
+# C H Forsyth and Vita Nuova Limited expressly allow Lucent Technologies
+# to use this driver freely for any Inferno-related purposes whatever,
+# including commercial applications.
+#
+# TO DO:
+#	check error handling details for get/put flash
+#	bad block handling
+#	reserved space in formatted size
+#	possibly block size as parameter
+#	fetch parameters from header on init
+#
+# Adapted to a ftl formatter for Inferno 2000 by J R Firth, Vita Nuova Limited
+#	usage : ftl flashsize secsize inputfile outputfile
+# outputfile will then be a ftl image of inputfile
+# nb assumes the base address is zero
+#
+# Converted to limbo for Inferno 2000 by JR Firth, Vita Nuova Holdings Limited
+#
+
+implement Ftlimage;
+
+include "sys.m";
+include "draw.m";
+
+sys : Sys;
+	OREAD, OWRITE, FD, open, create, read, write, print, fprint : import sys;
+
+Ftlimage : module
+{
+	init : fn(nil : ref Draw->Context, argv : list of string);
+};
+
+stderr : ref FD;
+
+flashsize, secsize : int;
+flashm : array of byte;
+trace : int = 0;
+
+Eshift : con 18;			# 2^18=256k; log2(eraseunit)
+Flashseg : con 1<<Eshift;
+Bshift : con 9;			# 2^9=512
+Bsize : con 1<<Bshift;
+BAMoffset : con 16r100;
+Nolimit : con ~0;
+USABLEPCT : con 95;	# release only this % to client
+
+FTLDEBUG : con 0;
+
+# erase unit header (defined by FTL specification)
+# offsets into Merase
+O_LINKTUPLE : con 0;
+O_ORGTUPLE : con 5;
+O_NXFER : con 15;
+O_NERASE : con 16;
+O_ID : con 20;
+O_BSHIFT : con 22;
+O_ESHIFT : con 23;
+O_PSTART : con 24;
+O_NUNITS : con 26;
+O_PSIZE : con 28;
+O_VBMBASE : con 32;
+O_NVBM : con 36;
+O_FLAGS : con 38;
+O_CODE : con 39;
+O_SERIAL : con 40;
+O_ALTOFFSET : con 44;
+O_BAMOFFSET : con 48;
+O_RSV2 : con 52;
+
+ERASEHDRLEN : con	64;
+
+# special unit IDs
+XferID : con 16rffff;
+XferBusy : con 16r7fff;
+
+# special BAM addresses
+Bfree : con -1;	#16rffffffff
+Bwriting : con -2; #16rfffffffe
+Bdeleted : con 0;
+
+# block types
+TypeShift : con 7;
+BlockType : con (1<<TypeShift)-1;
+ControlBlock : con 16r30;
+DataBlock : con 16r40;
+ReplacePage : con 16r60;
+BadBlock : con 16r70;
+
+BNO(va : int) : int
+{
+	return va>>Bshift;
+}
+MKBAM(b : int,t : int) : int
+{
+	return (b<<Bshift)|t;
+}
+
+Terase : adt {
+	x : int;
+	id : int;
+	offset : int;
+	bamoffset : int;
+	nbam : int;
+	bam : array of byte;
+	bamx : int;
+	nfree : int;
+	nused : int;
+	ndead : int;
+	nbad : int;
+	nerase : int;
+};
+
+Ftl : adt {
+	base : int;		# base of flash region 
+	size : int;		# size of flash region 
+	segsize : int;	# size of flash segment (erase unit) 
+	eshift : int;	# log2(erase-unit-size) 
+	bshift : int;	# log2(bsize) 
+	bsize : int;
+	nunit : int;		# number of segments (erase units) 
+	unit : array of ref Terase;
+	lastx : int;		# index in unit of last allocation 
+	xfer : int;		# index in unit of current transfer unit (-1 if none) 
+	nfree : int;		# total free space in blocks 
+	nblock : int;	# total space in blocks 
+	rwlimit : int;	# user-visible block limit (`formatted size') 
+	vbm : array of int;		# virtual block map
+	fstart : int;		# address of first block of data in a segment 
+	trace : int;		# (debugging) trace of read/write actions 
+	detach : int;	# free Ftl on last close 
+ 
+	# scavenging variables 
+	needspace : int;
+	hasproc : int;
+};
+
+# Ftl.detach 
+Detached : con 1;	# detach on close 
+Deferred : con 2;	# scavenger must free it 
+
+ftls : ref Ftl;
+
+ftlstat(sz : int)
+{
+	print("16r%x:16r%x:16r%x\n", ftls.rwlimit*Bsize, sz, flashsize);
+	print("%d:%d:%d in 512b blocks\n", ftls.rwlimit, sz>>Bshift, flashsize>>Bshift);
+}
+	 
+ftlread(buf : array of byte, n : int, offset : int) : int
+{
+	ftl : ref Ftl;
+	e : ref Terase;
+	nb : int;
+	a : int;
+	pb : int;
+	mapb : int;
+
+	if(n <= 0 || n%Bsize || offset%Bsize) {
+		fprint(stderr, "ftl: bad read\n");
+		exit;
+	}
+	ftl = ftls;
+	nb = n/Bsize;
+	offset /= Bsize;
+	if(offset >= ftl.rwlimit)
+		return 0;
+	if(offset+nb > ftl.rwlimit)
+		nb = ftl.rwlimit - offset;
+	a = 0;
+	for(n = 0; n < nb; n++){
+		(mapb, e, pb) = mapblk(ftl, offset+n);
+		if(mapb)
+			getflash(ftl, buf[a:], e.offset + pb*Bsize, Bsize);
+		else
+			memset(buf[a:], 0, Bsize);
+		a += Bsize;
+	}
+	return a;
+}
+
+ftlwrite(buf : array of byte, n : int, offset : int) : int
+{
+	ns, nb : int;
+	a : int;
+	e, oe : ref Terase;
+	ob, v : int;
+	ftl : ref Ftl;
+	mapb : int;
+
+	if(n <= 0)
+		return 0;
+	ftl = ftls;
+	if(n <= 0 || n%Bsize || offset%Bsize) {
+		fprint(stderr, "ftl: bad write\n");
+		exit;
+	}
+	nb = n/Bsize;
+	offset /= Bsize;
+	if(offset >= ftl.rwlimit)
+		return 0;
+	if(offset+nb > ftl.rwlimit)
+		nb = ftl.rwlimit - offset;
+	a = 0;
+	for(n = 0; n < nb; n++){
+		ns = 0;
+		while((v = allocblk(ftl)) == 0)
+			if(!scavenge(ftl) || ++ns > 3){
+				fprint(stderr, "ftl: flash memory full\n");
+			}
+		(mapb, oe, ob) = mapblk(ftl, offset+n);
+		if(!mapb)
+			oe = nil;
+		e = ftl.unit[v>>16];
+		v &= 16rffff;
+		putflash(ftl, e.offset + v*Bsize, buf[a:], Bsize);
+		putbam(ftl, e, v, MKBAM(offset+n, DataBlock));
+		# both old and new block references exist in this window (can't be closed?) 
+		ftl.vbm[offset+n] = (e.x<<16) | v;
+		if(oe != nil){
+			putbam(ftl, oe, ob, Bdeleted);
+			oe.ndead++;
+		}
+		a += Bsize;
+	}
+	return a;
+}
+
+mkftl(fname : string, base : int, size : int, eshift : int, op : string) : ref Ftl
+{
+	i, j, nov, segblocks : int;
+	limit : int;
+	e : ref Terase;
+
+	ftl := ref Ftl;
+	ftl.lastx = 0;
+	ftl.detach = 0;
+	ftl.needspace = 0;
+	ftl.hasproc = 0;
+	ftl.trace = 0;
+	limit = flashsize;
+	if(size == Nolimit)
+		size = limit-base;
+	if(base >= limit || size > limit || base+size > limit || eshift < 8 || (1<<eshift) > size) {
+		fprint(stderr, "bad flash space parameters");
+		exit;
+	}
+	if(FTLDEBUG || ftl.trace || trace)
+		print("%s flash %s #%x:#%x limit #%x\n", op, fname, base, size, limit);
+	ftl.base = base;
+	ftl.size = size;
+	ftl.bshift = Bshift;
+	ftl.bsize = Bsize;
+	ftl.eshift = eshift;
+	ftl.segsize = 1<<eshift;
+	ftl.nunit = size>>eshift;
+	nov = ((ftl.segsize/Bsize)*4 + BAMoffset + Bsize - 1)/Bsize;	# number of overhead blocks per segment (header, and BAM itself) 
+	ftl.fstart = nov;
+	segblocks = ftl.segsize/Bsize - nov;
+	ftl.nblock = ftl.nunit*segblocks;
+	if(ftl.nblock >= 16r10000)
+		ftl.nblock = 16r10000;
+	ftl.vbm = array[ftl.nblock] of int; 
+	ftl.unit = array[ftl.nunit] of ref Terase;
+	if(ftl.vbm == nil || ftl.unit == nil) {
+		fprint(stderr, "out of mem");
+		exit;
+	}
+	for(i=0; i<ftl.nblock; i++)
+		ftl.vbm[i] = 0;
+	if(op == "format"){
+		for(i=0; i<ftl.nunit-1; i++)
+			eraseinit(ftl, i*ftl.segsize, i, 1);
+		eraseinit(ftl, i*ftl.segsize, XferID, 1);
+	}
+	ftl.xfer = -1;
+	for(i=0; i<ftl.nunit; i++){
+		e = eraseload(ftl, i, i*ftl.segsize);
+		if(e == nil){
+			fprint(stderr, "ftl: logical segment %d: bad format\n", i);
+			continue;
+		}
+		if(e.id == XferBusy){
+			e.nerase++;
+			eraseinit(ftl, e.offset, XferID, e.nerase);
+			e.id = XferID;
+		}
+		for(j=0; j<ftl.nunit; j++)
+			if(ftl.unit[j] != nil && ftl.unit[j].id == e.id){
+				fprint(stderr, "ftl: duplicate erase unit #%x\n", e.id);
+				erasefree(e);
+				e = nil;
+				break;
+			}
+		if(e != nil){
+			ftl.unit[e.x] = e;
+			if(e.id == XferID)
+				ftl.xfer = e.x;
+			if (FTLDEBUG || ftl.trace || trace)
+				fprint(stderr, "ftl: unit %d:#%x used %d free %d dead %d bad %d nerase %d\n",
+					e.x, e.id, e.nused, e.nfree, e.ndead, e.nbad, e.nerase);
+		}
+	}
+	if(ftl.xfer < 0 && ftl.nunit <= 0 || ftl.xfer >= 0 && ftl.nunit <= 1) {
+		fprint(stderr, "ftl: no valid flash data units");
+		exit;
+	}
+	if(ftl.xfer < 0)
+		fprint(stderr, "ftl: no transfer unit: device is WORM\n");
+	else
+		ftl.nblock -= segblocks;	# discount transfer segment 
+	if(ftl.nblock >= 1000)
+		ftl.rwlimit = ftl.nblock-100;	# TO DO: variable reserve 
+	else
+		ftl.rwlimit = ftl.nblock*USABLEPCT/100;
+	return ftl;
+}
+
+ftlfree(ftl : ref Ftl)
+{
+	if(ftl != nil){
+		ftl.unit = nil;
+		ftl.vbm = nil;
+		ftl = nil;
+	}
+}
+
+#
+# this simple greedy algorithm weighted by nerase does seem to lead
+# to even wear of erase units (cf. the eNVy file system)
+#
+ 
+bestcopy(ftl : ref Ftl) : ref Terase
+{
+	e, be : ref Terase;
+	i : int;
+
+	be = nil;
+	for(i=0; i<ftl.nunit; i++)
+		if((e = ftl.unit[i]) != nil && e.id != XferID && e.id != XferBusy && e.ndead+e.nbad &&
+		    (be == nil || e.nerase <= be.nerase && e.ndead >= be.ndead))
+			be = e;
+	return be;
+}
+
+copyunit(ftl : ref Ftl, from : ref Terase, too : ref Terase) : int
+{
+	i, nb : int;
+	id := array[2] of byte;
+	bam : array of byte;
+	buf : array of byte;
+	v, bno : int;
+
+	if(FTLDEBUG || ftl.trace || trace)
+		print("ftl: copying %d (#%x) to #%x\n", from.id, from.offset, too.offset);
+	too.nbam = 0;
+	too.bam = nil;
+	bam = nil;
+	buf = array[Bsize] of byte;
+	if(buf == nil)
+		return 0;
+	PUT2(id, XferBusy);
+	putflash(ftl, too.offset+O_ID, id, 2);
+	# make new BAM 
+	nb = from.nbam*4;
+	bam = array[nb] of byte;
+	memmove(bam, from.bam, nb);
+	too.nused = 0;
+	too.nbad = 0;
+	too.nfree = 0;
+	too.ndead = 0;
+	for(i = 0; i < from.nbam; i++)
+		bv := GET4(bam[4*i:]);
+		case(bv){
+		Bwriting or
+		Bdeleted or
+		Bfree =>
+			PUT4(bam[4*i:], Bfree);
+			too.nfree++;
+			break;
+		* =>
+			case(bv&BlockType){
+			DataBlock or
+			ReplacePage =>
+				v = bv;
+				bno = BNO(v & ~BlockType);
+				if(i < ftl.fstart || bno >= ftl.nblock){
+					print("ftl: unit %d:#%x bad bam[%d]=#%x\n", from.x, from.id, i, v);
+					too.nfree++;
+					PUT4(bam[4*i:], Bfree);
+					break;
+				}
+				getflash(ftl, buf, from.offset+i*Bsize, Bsize);
+				putflash(ftl, too.offset+i*Bsize, buf, Bsize);
+				too.nused++;
+				break;
+			ControlBlock =>
+				too.nused++;
+				break;
+			* =>
+				# case BadBlock:	# it isn't necessarily bad in this unit 
+				too.nfree++;
+				PUT4(bam[4*i:], Bfree);
+				break;
+			}
+		}
+	# for(i=0; i<from.nbam; i++){
+	#	v = GET4(bam[4*i:]);
+	#	if(v != Bfree && ftl.trace > 1)
+	#		print("to[%d]=#%x\n", i, v);
+	#	PUT4(bam[4*i:], v);
+	# }
+	putflash(ftl, too.bamoffset, bam, nb);	# BUG: PUT4 ? IS IT ?
+	# for(i=0; i<from.nbam; i++){
+	#	v = GET4(bam[4*i:]);
+	#	PUT4(bam[4*i:], v);
+	# }
+	too.id = from.id;
+	PUT2(id, too.id);
+	putflash(ftl, too.offset+O_ID, id, 2);
+	too.nbam = from.nbam;
+	too.bam = bam;
+	ftl.nfree += too.nfree - from.nfree;
+	buf = nil;
+	return 1;
+}
+
+mustscavenge(a : ref Ftl) : int
+{
+	return a.needspace || a.detach == Deferred;
+}
+
+donescavenge(a : ref Ftl) : int
+{
+	return a.needspace == 0;
+}
+
+scavengeproc(arg : ref Ftl)
+{
+	ftl : ref Ftl;
+	i : int;
+	e, ne : ref Terase;
+
+	ftl = arg;
+	if(mustscavenge(ftl)){
+		if(ftl.detach == Deferred){
+			ftlfree(ftl);
+			fprint(stderr, "scavenge out of memory\n");
+			exit;
+		}
+		if(FTLDEBUG || ftl.trace || trace)
+			print("ftl: scavenge %d\n", ftl.nfree);
+		e = bestcopy(ftl);
+		if(e == nil || ftl.xfer < 0 || (ne = ftl.unit[ftl.xfer]) == nil || ne.id != XferID || e == ne)
+			;
+		else if(copyunit(ftl, e, ne)){
+			i = ne.x; ne.x = e.x; e.x = i;
+			ftl.unit[ne.x] = ne;
+			ftl.unit[e.x] = e;
+			ftl.xfer = e.x;
+			e.id = XferID;
+			e.nbam = 0;
+			e.bam = nil;
+			e.bamx = 0;
+			e.nerase++;
+			eraseinit(ftl, e.offset, XferID, e.nerase);
+		}
+		if(FTLDEBUG || ftl.trace || trace)
+			print("ftl: end scavenge %d\n", ftl.nfree);
+		ftl.needspace = 0;
+	}
+}
+
+scavenge(ftl : ref Ftl) : int
+{
+	if(ftl.xfer < 0 || bestcopy(ftl) == nil)
+		return 0;	# you worm! 
+
+	if(!ftl.hasproc){
+		ftl.hasproc = 1;
+	}
+	ftl.needspace = 1;
+
+	scavengeproc(ftls);
+
+	return ftl.nfree;
+}
+
+putbam(ftl : ref Ftl, e : ref Terase, n : int, entry : int)
+{
+	b := array[4] of byte;
+
+	PUT4(e.bam[4*n:], entry);
+	PUT4(b, entry);
+	putflash(ftl, e.bamoffset + n*4, b, 4);
+}
+
+allocblk(ftl : ref Ftl) : int
+{
+	e : ref Terase;
+	i, j : int;
+
+	i = ftl.lastx;
+	do{
+		e = ftl.unit[i];
+		if(e != nil && e.id != XferID && e.nfree){
+			ftl.lastx = i;
+			for(j=e.bamx; j<e.nbam; j++)
+				if(GET4(e.bam[4*j:])== Bfree){
+					putbam(ftl, e, j, Bwriting);
+					ftl.nfree--;
+					e.nfree--;
+					e.bamx = j+1;
+					return (e.x<<16) | j;
+				}
+			e.nfree = 0;
+			print("ftl: unit %d:#%x nfree %d but not free in BAM\n", e.x, e.id, e.nfree);
+		}
+		if(++i >= ftl.nunit)
+			i = 0;
+	}while(i != ftl.lastx);
+	return 0;
+}
+
+mapblk(ftl : ref Ftl, bno : int) : (int, ref Terase, int)
+{
+	v : int;
+	x : int;
+
+	if(bno < ftl.nblock){
+		v = ftl.vbm[bno];
+		if(v == 0 || v == ~0)
+			return (0, nil, 0);
+		x = v>>16;
+		if(x >= ftl.nunit || x == ftl.xfer || ftl.unit[x] == nil){
+			print("ftl: corrupt format: bad block mapping %d . unit #%x\n", bno, x);
+			return (0, nil, 0);
+		}
+		return (1, ftl.unit[x], v & 16rFFFF);
+	}
+	return (0, nil, 0);
+}
+
+eraseinit(ftl : ref Ftl, offset : int, id : int, nerase : int)
+{
+	m : array of byte;
+	bam : array of byte;
+	i, nov : int;
+
+	nov = ((ftl.segsize/Bsize)*4 + BAMoffset + Bsize - 1)/Bsize;	# number of overhead blocks (header, and BAM itself) 
+	if(nov*Bsize >= ftl.segsize) {
+		fprint(stderr, "ftl -- too small for files");
+		exit;
+	}
+	eraseflash(ftl, offset);
+	m = array[ERASEHDRLEN] of byte;
+	if(m == nil) {
+		fprint(stderr, "nomem\n");
+		exit;
+	}
+	memset(m, 16rFF, len m);
+	m[O_LINKTUPLE+0] = byte 16r13;
+	m[O_LINKTUPLE+1] = byte 16r3;
+	memmove(m[O_LINKTUPLE+2:], array of byte "CIS", 3);
+	m[O_ORGTUPLE+0] = byte 16r46;
+	m[O_ORGTUPLE+1] = byte 16r57;
+	m[O_ORGTUPLE+2] = byte 16r00;
+	memmove(m[O_ORGTUPLE+3:], array of byte "FTL100\0", 7);
+	m[O_NXFER] = byte 1;
+	PUT4(m[O_NERASE:], nerase);
+	PUT2(m[O_ID:], id);
+	m[O_BSHIFT] = byte ftl.bshift;
+	m[O_ESHIFT] = byte ftl.eshift;
+	PUT2(m[O_PSTART:], 0);
+	PUT2(m[O_NUNITS:], ftl.nunit);
+	PUT4(m[O_PSIZE:], ftl.size - nov*Bsize);
+	PUT4(m[O_VBMBASE:], -1);	# we always calculate the VBM (16rffffffff)
+	PUT2(m[O_NVBM:], 0);
+	m[O_FLAGS] = byte 0;
+	m[O_CODE] = byte 16rFF;
+	memmove(m[O_SERIAL:], array of byte "Inf1", 4);
+	PUT4(m[O_ALTOFFSET:], 0);
+	PUT4(m[O_BAMOFFSET:], BAMoffset);
+	putflash(ftl, offset, m, ERASEHDRLEN);
+	m = nil;
+	if(id == XferID)
+		return;
+	nov *= 4;	# now bytes of BAM 
+	bam = array[nov] of byte;
+	if(bam == nil) {
+		fprint(stderr, "nomem");
+		exit;
+	}
+	for(i=0; i<nov; i += 4)
+		PUT4(bam[i:], ControlBlock);	# reserve them 
+	putflash(ftl, offset+BAMoffset, bam, nov);
+	bam = nil;
+}
+
+eraseload(ftl : ref Ftl, x : int, offset : int) : ref Terase
+{
+	m : array of byte;
+	e : ref Terase;
+	i, nbam : int;
+	bno, v : int;
+
+	m = array[ERASEHDRLEN] of byte;
+	if(m == nil) {
+		fprint(stderr, "nomem");
+		exit;
+	}
+	getflash(ftl, m, offset, ERASEHDRLEN);
+	if(memcmp(m[O_ORGTUPLE+3:], array of byte "FTL100\0", 7) != 0 ||
+	   memcmp(m[O_SERIAL:], array of byte "Inf1", 4) != 0){
+		m = nil;
+		return nil;
+	}
+	e = ref Terase;
+	if(e == nil){
+		m = nil;
+		fprint(stderr, "nomem");
+		exit;
+	}
+	e.x = x;
+	e.id = GET2(m[O_ID:]);
+	e.offset = offset;
+	e.bamoffset = GET4(m[O_BAMOFFSET:]);
+	e.nerase = GET4(m[O_NERASE:]);
+	e.bamx = 0;
+	e.nfree = 0;
+	e.nused = 0;
+	e.ndead = 0;
+	e.nbad = 0;
+	m = nil;
+	if(e.bamoffset != BAMoffset){
+		e = nil;
+		return nil;
+	}
+	e.bamoffset += offset;
+	if(e.id == XferID || e.id == XferBusy){
+		e.bam = nil;
+		e.nbam = 0;
+		return e;
+	}
+	nbam = ftl.segsize/Bsize;
+	e.bam = array[4*nbam] of byte;
+	e.nbam = nbam;
+	getflash(ftl, e.bam, e.bamoffset, nbam*4);
+	# scan BAM to build VBM 
+	e.bamx = 0;
+	for(i=0; i<nbam; i++){
+		v = GET4(e.bam[4*i:]);
+		if(v == Bwriting || v == Bdeleted)
+			e.ndead++;
+		else if(v == Bfree){
+			if(e.bamx == 0)
+				e.bamx = i;
+			e.nfree++;
+			ftl.nfree++;
+		}else{
+			case(v & BlockType){
+			ControlBlock =>
+				break;
+			DataBlock =>
+				# add to VBM 
+				if(v & (1<<31))
+					break;	# negative => VBM page, ignored 
+				bno = BNO(v & ~BlockType);
+				if(i < ftl.fstart || bno >= ftl.nblock){
+					print("ftl: unit %d:#%x bad bam[%d]=#%x\n", e.x, e.id, i, v);
+					e.nbad++;
+					break;
+				}
+				ftl.vbm[bno] = (e.x<<16) | i;
+				e.nused++;
+				break;
+			ReplacePage =>
+				# replacement VBM page; ignored 
+				break;
+			BadBlock =>
+				e.nbad++;
+				break;
+			* =>
+				print("ftl: unit %d:#%x bad bam[%d]=%x\n", e.x, e.id, i, v);
+			}
+		}
+	}
+	return e;
+}
+
+erasefree(e : ref Terase)
+{
+	e.bam = nil;
+	e = nil;
+}
+
+eraseflash(ftl : ref Ftl, offset : int)
+{
+	offset += ftl.base;
+	if(FTLDEBUG || ftl.trace || trace)
+		print("ftl: erase seg @#%x\n", offset);
+	memset(flashm[offset:], 16rff, secsize);
+}
+
+putflash(ftl : ref Ftl, offset : int, buf : array of byte, n : int)
+{
+	offset += ftl.base;
+	if(ftl.trace || trace)
+		print("ftl: write(#%x, %d)\n", offset, n);
+	memmove(flashm[offset:], buf, n);
+}
+
+getflash(ftl : ref Ftl, buf : array of byte, offset : int, n : int)
+{
+	offset += ftl.base;
+	if(ftl.trace || trace)
+		print("ftl: read(#%x, %d)\n", offset, n);
+	memmove(buf, flashm[offset:], n);
+}
+
+BUFSIZE : con 8192;
+
+main(argv : list of string)
+{
+	k, r, sz, offset : int = 0;
+	buf, buf1 : array of byte;
+	fd1, fd2 : ref FD;
+
+	if (len argv != 5) {
+		fprint(stderr, "usage: %s flashsize secsize kfsfile flashfile\n", hd argv);
+		exit;
+	}
+	flashsize = atoi(hd tl argv);
+	secsize = atoi(hd tl tl argv);
+	fd1 = open(hd tl tl tl argv, OREAD);
+	fd2 = create(hd tl tl tl tl argv, OWRITE, 8r644);
+	if (fd1 == nil || fd2 == nil) {
+		fprint(stderr, "bad io files\n");
+		exit;
+	}
+	if(secsize == 0 || secsize > flashsize || secsize&(secsize-1) || 0&(secsize-1) || flashsize == 0 || flashsize != Nolimit && flashsize&(secsize-1)) {
+		fprint(stderr, "ftl: bad sizes\n");
+		exit;
+	}
+	for(k=0; k<32 && (1<<k) != secsize; k++)
+			;
+	flashm = array[flashsize] of byte;
+	buf = array[BUFSIZE] of byte;
+	if (flashm == nil) {
+		fprint(stderr, "ftl: no mem for flash\n");
+		exit;
+	}
+	ftls = mkftl("FLASH", 0, Nolimit, k, "format");
+	for (;;) {
+		r = read(fd1, buf, BUFSIZE);
+		if (r <= 0)
+			break;
+		if (ftlwrite(buf, r, offset) != r) {
+			fprint(stderr, "ftl: ftlwrite failed - input file too big\n");
+			exit;
+		}
+		offset += r;
+	}
+	write(fd2, flashm, flashsize);
+	fd1 = fd2 = nil;
+	ftlstat(offset);
+	# ftls = mkftl("FLASH", 0, Nolimit, k, "init"); 
+	sz = offset;
+	offset = 0;
+	buf1 = array[BUFSIZE] of byte;
+	fd1 = open(hd tl tl tl argv, OREAD);
+	for (;;) {
+		r = read(fd1, buf1, BUFSIZE);
+		if (r <= 0)
+			break;
+		if (ftlread(buf, r, offset) != r) {
+			fprint(stderr, "ftl: ftlread failed\n");
+			exit;
+		}
+		if (memcmp(buf, buf1, r) != 0) {
+			fprint(stderr, "ftl: bad read\n");
+			exit;
+		}
+		offset += r;
+	}
+	fd1 = nil;
+	if (offset != sz) {
+		fprint(stderr, "ftl: bad final offset\n");
+		exit;
+	}
+	exit;
+}
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	main(argl);
+}
+
+memset(d : array of byte, v : int, n : int)
+{
+	for (i := 0; i < n; i++)
+		d[i] = byte v;
+}
+
+memmove(d : array of byte, s : array of byte, n : int)
+{
+	d[0:] = s[0:n];
+}
+
+memcmp(s1 : array of byte, s2 : array of byte, n : int) : int
+{
+	for (i := 0; i < n; i++) {
+		if (s1[i] < s2[i])
+			return -1;
+		if (s1[i] > s2[i])
+			return 1;
+	}
+	return 0;
+}
+
+atoi(s : string) : int
+{
+	v : int;
+	base := 10;
+	n := len s;
+	neg := 0;
+
+	for (i := 0; i < n && (s[i] == ' ' || s[i] == '\t'); i++)
+		;
+	if (s[i] == '+' || s[i] == '-') {
+		if (s[i] == '-')
+			neg = 1;
+		i++;
+	}
+	if (n-i >= 2 && s[i] == '0' && s[i+1] == 'x') {
+		base = 16;
+		i += 2;
+	}
+	else if (n-i >= 1 && s[i] == '0') {
+		base = 8;
+		i++;
+	}
+	m := 0;
+	for(; i < n; i++) {
+		c := s[i];
+		case c {
+		'a' to 'z' =>
+			v = c - 'a' + 10;
+		'A' to 'Z' =>
+			v = c - 'A' + 10;
+		'0' to '9' =>
+			v = c - '0';
+		* =>
+			fprint(stderr, "ftl: bad character in number %s\n", s);
+			exit;
+		}
+		if(v >= base) {
+			fprint(stderr, "ftl: character too big for base in %s\n", s);
+			exit;
+		}
+		m = m * base + v;
+	}
+	if(neg)
+		m = -m;
+	return m;
+}
+
+# little endian 
+
+GET2(b : array of byte) : int
+{
+	return ((int b[1]) << 8) | (int b[0]);
+}
+
+GET4(b : array of byte) : int
+{
+	return ((int b[3]) << 24) | ((int b[2]) << 16) | ((int b[1]) << 8) | (int b[0]);
+}
+
+PUT2(b : array of byte, v : int)
+{
+	b[1] = byte (v>>8);
+	b[0] = byte v;
+}
+
+PUT4(b : array of byte, v : int)
+{
+	b[3] = byte (v>>24);
+	b[2] = byte (v>>16);
+	b[1] = byte (v>>8);
+	b[0] = byte v;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/kfs.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,3844 @@
+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);
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/kfscmd.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,53 @@
+implement Kfscmd;
+
+include "sys.m";
+	sys:	Sys;
+
+include "draw.m";
+include "arg.m";
+
+Kfscmd: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		err(sys->sprint("can't load %s: %r", Arg->PATH));
+
+	cfs := "main";
+	arg->init(args);
+	arg->setusage("disk/kfscmd [-n fsname] cmd ...");
+	while((c := arg->opt()) != 0)
+		case c {
+		'n' =>
+			cfs = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	ctlf := "/chan/kfs."+cfs+".cmd";
+	ctl := sys->open(ctlf, Sys->ORDWR);
+	if(ctl == nil)
+		err(sys->sprint("can't open %s: %r", ctlf));
+	for(; args != nil; args = tl args){
+		if(sys->fprint(ctl, "%s", hd args) > 0){
+			buf := array[1024] of byte;
+			while((n := sys->read(ctl, buf, len buf)) > 0)
+				sys->write(sys->fildes(1), buf, n);
+		}else
+			err(sys->sprint("%q: %r", hd args));
+	}
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "kfscmd: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/mbr.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,134 @@
+implement Mbr;
+
+#
+# install new master boot record boot code on PC disk.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "disks.m";
+	disks: Disks;
+	Disk, PCpart, Toffset: import disks;
+
+include "arg.m";
+
+Mbr: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+
+
+#
+# Default boot block prints an error message and reboots. 
+#
+ndefmbr := Toffset;
+defmbr := array[512] of {
+	byte 16rEB, byte 16r3C, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00,
+	byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00, byte 16r00,
+16r03E => byte 16rFA, byte 16rFC, byte 16r8C, byte 16rC8, byte 16r8E, byte 16rD8, byte 16r8E, byte 16rD0,
+	byte 16rBC, byte 16r00, byte 16r7C, byte 16rBE, byte 16r77, byte 16r7C, byte 16rE8, byte 16r19,
+	byte 16r00, byte 16r33, byte 16rC0, byte 16rCD, byte 16r16, byte 16rBB, byte 16r40, byte 16r00,
+	byte 16r8E, byte 16rC3, byte 16rBB, byte 16r72, byte 16r00, byte 16rB8, byte 16r34, byte 16r12,
+	byte 16r26, byte 16r89, byte 16r07, byte 16rEA, byte 16r00, byte 16r00, byte 16rFF, byte 16rFF,
+	byte 16rEB, byte 16rD6, byte 16rAC, byte 16r0A, byte 16rC0, byte 16r74, byte 16r09, byte 16rB4,
+	byte 16r0E, byte 16rBB, byte 16r07, byte 16r00, byte 16rCD, byte 16r10, byte 16rEB, byte 16rF2,
+	byte 16rC3,  byte 'N',  byte 'o',  byte 't',  byte ' ',  byte 'a',  byte ' ',  byte 'b',
+	 byte 'o',  byte 'o',  byte 't',  byte 'a',  byte 'b',  byte 'l',  byte 'e',  byte ' ',
+	 byte 'd',  byte 'i',  byte 's',  byte 'c',  byte ' ',  byte 'o',  byte 'r',  byte ' ',
+	 byte 'd',  byte 'i',  byte 's',  byte 'c',  byte ' ',  byte 'e',  byte 'r',  byte 'r',
+	 byte 'o',  byte 'r', byte '\r', byte '\n',  byte 'P',  byte 'r',  byte 'e',  byte 's',
+	 byte 's',  byte ' ',  byte 'a',  byte 'l',  byte 'm',  byte 'o',  byte 's',  byte 't',
+	 byte ' ',  byte 'a',  byte 'n',  byte 'y',  byte ' ',  byte 'k',  byte 'e',  byte 'y',
+	 byte ' ',  byte 't',  byte 'o',  byte ' ',  byte 'r',  byte 'e',  byte 'b',  byte 'o',
+	 byte 'o',  byte 't',  byte '.',  byte '.',  byte '.', byte 16r00, byte 16r00, byte 16r00,
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	flag9 := 0;
+	mbrfile: string;
+	sys = load Sys Sys->PATH;
+	disks = load Disks Disks->PATH;
+
+	sys->pctl(Sys->FORKFD, nil);
+	disks->init();
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("disk/mbr [-m mbrfile] disk");
+	while((o := arg->opt()) != 0)
+		case o {
+		'9' =>
+			flag9 = 1;
+		'm' =>
+			mbrfile = arg->earg();
+		* =>
+			arg->usage();
+		} 
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	arg = nil;
+
+	disk := Disk.open(hd args, Sys->ORDWR, 0);
+	if(disk == nil)
+		fatal(sys->sprint("opendisk %s: %r", hd args));
+
+	if(disk.dtype == "floppy")
+		fatal(sys->sprint("will not install mbr on floppy"));
+	if(disk.secsize != 512)
+		fatal(sys->sprint("secsize %d invalid: must be 512", disk.secsize));
+
+	secsize := disk.secsize;
+	mbr := array[secsize*disk.s] of {* => byte 0};
+
+	#
+	# Start with initial sector from disk.
+	#
+	if(sys->seek(disk.fd, big 0, 0) < big 0)
+		fatal(sys->sprint("seek to boot sector: %r\n"));
+	if(sys->read(disk.fd, mbr, secsize) != secsize)
+		fatal(sys->sprint("reading boot sector: %r"));
+
+	nmbr: int;
+	if(mbrfile == nil){
+		nmbr = ndefmbr;
+		mbr[0:] = defmbr;
+	} else {
+		buf := array[secsize*(disk.s+1)] of {* => byte 0};
+		if((sysfd := sys->open(mbrfile, Sys->OREAD)) == nil)
+			fatal(sys->sprint("open %s: %r", mbrfile));
+		if((nmbr = sys->read(sysfd, buf, secsize*(disk.s+1))) < 0)
+			fatal(sys->sprint("read %s: %r", mbrfile));
+		if(nmbr > secsize*disk.s)
+			fatal(sys->sprint("master boot record too large %d > %d", nmbr, secsize*disk.s));
+		if(nmbr < secsize)
+			nmbr = secsize;
+		sysfd = nil;
+		buf[Toffset:] = mbr[Toffset:secsize];
+		mbr[0:] = buf[0:nmbr];
+	}
+
+	if(flag9){
+		for(i := Toffset; i < secsize; i++)
+			mbr[i] = byte 0;
+		mbr[Toffset:] = PCpart(0, Disks->Type9, big 0, big disk.s, disk.secs-big disk.s).bytes(disk);
+	}
+	mbr[secsize-2] = byte Disks->Magic0;
+	mbr[secsize-1] = byte Disks->Magic1;
+	nmbr = (nmbr+secsize-1)&~(secsize-1);
+	if(sys->seek(disk.wfd, big 0, 0) < big 0)
+		fatal(sys->sprint("seek to MBR sector: %r\n"));
+	if(sys->write(disk.wfd, mbr, nmbr) != nmbr)
+		fatal(sys->sprint("writing MBR: %r"));
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "disk/mbr: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/mkext.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,352 @@
+implement Mkext;
+
+include "sys.m";
+	sys: Sys;
+	Dir, sprint, fprint: import sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+	arg: Arg;
+
+Mkext: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+LEN: con Sys->ATOMICIO;
+NFLDS: con 6;		# filename, modes, uid, gid, mtime, bytes
+
+bin: ref Iobuf;
+uflag := 0;
+tflag := 0;
+hflag := 0;
+vflag := 0;
+fflag := 0;
+stderr: ref Sys->FD;
+bout: ref Iobuf;
+argv0 := "mkext";
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		error(sys->sprint("cannot load %s: %r\n", Bufio->PATH));
+
+	str = load String String->PATH;
+	if(str == nil)
+		error(sys->sprint("cannot load %s: %r\n", String->PATH));
+
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		error(sys->sprint("cannot load %s: %r\n", Arg->PATH));
+
+	destdir := "";
+	arg->init(args);
+	arg->setusage("mkext [-h] [-d destdir] [-T] [-u] [-v] [file ...]");
+	while((c := arg->opt()) != 0)
+		case c {
+		'd' =>
+			destdir = arg->earg();
+		'f' =>
+			fflag = 1;
+
+		'h' =>
+			hflag = 1;
+			bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+			if(bout == nil)
+				error(sys->sprint("can't access standard output: %r"));
+		'u' =>
+			uflag = 1;
+			tflag = 1;
+		't' or 'T' =>
+			tflag = 1;
+		'v' =>
+			vflag = 1;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+
+	bin = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	if(bin == nil)
+		error(sys->sprint("can't access standard input: %r"));
+	while((p := bin.gets('\n')) != nil){
+		if(p == "end of archive\n"){
+			fprint(stderr, "done\n");
+			quit(nil);
+		}
+		fields := str->unquoted(p);
+		if(len fields != NFLDS){
+			warn("too few fields in file header");
+			continue;
+		}
+		name := hd fields;
+		fields = tl fields;
+		(mode, nil) := str->toint(hd fields, 8);
+		fields = tl fields;
+		uid := hd fields;
+		fields = tl fields;
+		gid := hd fields;
+		fields = tl fields;
+		(mtime, nil) := str->toint(hd fields, 10);
+		fields = tl fields;
+		(bytes, nil) := str->tobig(hd fields, 10);
+		if(args != nil){
+			if(!selected(name, args)){
+				if(bytes != big 0)
+					seekpast(bytes);
+				continue;
+			}
+			mkdirs(destdir, name);
+		}
+		name = destdir+name;
+		if(hflag){
+			bout.puts(sys->sprint("%q %s %s %s %ud %bd\n",
+				name, octal(mode), uid, gid, mtime, bytes));
+			if(bytes != big 0)
+				seekpast(bytes);
+			continue;
+		}
+		if(mode & Sys->DMDIR)
+			mkdir(name, mode, mtime, uid, gid);
+		else
+			extract(name, mode, mtime, uid, gid, bytes);
+	}
+	fprint(stderr, "premature end of archive\n");
+	quit("eof");
+}
+
+quit(s: string)
+{
+	if(bout != nil)
+		bout.flush();
+	if(s != nil)
+		raise "fail: "+s;
+	exit;
+}
+
+fileprefix(prefix, s: string): int
+{
+	n := len prefix;
+	m := len s;
+	if(n > m || !str->prefix(prefix, s))
+		return 0;
+	if(m > n && s[n] != '/')
+		return 0;
+	return 1;
+}
+
+selected(s: string, args: list of string): int
+{
+	for(; args != nil; args = tl args)
+		if(fileprefix(hd args, s))
+			return 1;
+	return 0;
+}
+
+mkdirs(basedir, name: string)
+{
+	(nil, names) := sys->tokenize(name, "/");
+	while(names != nil) {
+		#sys->print("mkdir %s\n", basedir);
+		create(basedir, Sys->OREAD, 8r775|Sys->DMDIR);
+
+		if(tl names == nil)
+			break;
+		basedir = basedir + "/" + hd names;
+		names = tl names;
+	}
+}
+
+mkdir(name: string, mode: int, mtime: int, uid: string, gid: string)
+{
+	d: Dir;
+	i: int;
+
+	fd := create(name, Sys->OREAD, mode);
+	if(fd == nil){
+		(i, d) = sys->stat(name);
+		if(i < 0 || !(d.mode & Sys->DMDIR)){
+			warn(sys->sprint("can't make directory %s: %r", name));
+			return;
+		}
+	}else{
+		(i, d) = sys->fstat(fd);
+		if(i < 0)
+			warn(sys->sprint("can't stat %s: %r", name));
+		fd = nil;
+	}
+
+	d = sys->nulldir;
+	(nil, p) := str->splitr(name, "/");
+	if(p == nil)
+		p = name;
+	d.name = p;
+	if(tflag)
+		d.mtime = mtime;
+	if(uflag){
+		d.uid = uid;
+		d.gid = gid;
+	}
+	d.mode = mode;
+	if(sys->wstat(name, d) < 0)
+		warn(sys->sprint("can't set modes for %s: %r", name));
+	if(uflag){
+		(i, d) = sys->stat(name);
+		if(i < 0)
+			warn(sys->sprint("can't reread modes for %s: %r", name));
+		if(d.mtime != mtime)
+			warn(sys->sprint("%s: time mismatch %ud %ud\n", name, mtime, d.mtime));
+		if(uid != d.uid)
+			warn(sys->sprint("%s: uid mismatch %s %s", name, uid, d.uid));
+		if(gid != d.gid)
+			warn(sys->sprint("%s: gid mismatch %s %s", name, gid, d.gid));
+	}
+}
+
+extract(name: string, mode: int, mtime: int, uid: string, gid: string, bytes: big)
+{
+	n: int;
+
+	if(vflag)
+		sys->print("x %s %bd bytes\n", name, bytes);
+
+	sfd := create(name, Sys->OWRITE, mode);
+	if(sfd == nil) {
+		if(!fflag || sys->remove(name) == -1 ||
+		    (sfd = create(name, Sys->OWRITE, mode)) == nil) {
+			warn(sys->sprint("can't make file %s: %r", name));
+			seekpast(bytes);
+			return;
+		}
+	}
+	b := bufio->fopen(sfd, Bufio->OWRITE);
+	if (b == nil) {
+		warn(sys->sprint("can't open file %s for bufio : %r", name));
+		seekpast(bytes);
+		return;
+	}
+	buf := array [LEN] of byte;
+	for(tot := big 0; tot < bytes; tot += big n){
+		n = len buf;
+		if(tot + big n > bytes)
+			n = int(bytes - tot);
+		n = bin.read(buf, n);
+		if(n <= 0)
+			error(sys->sprint("premature eof reading %s", name));
+		if(b.write(buf, n) != n)
+			warn(sys->sprint("error writing %s: %r", name));
+	}
+
+	(i, nil) := sys->fstat(b.fd);
+	if(i < 0)
+		warn(sys->sprint("can't stat %s: %r", name));
+	d := sys->nulldir;
+	(nil, p) := str->splitr(name, "/");
+	if(p == nil)
+		p = name;
+	d.name = p;
+	if(tflag)
+		d.mtime = mtime;
+	if(uflag){
+		d.uid = uid;
+		d.gid = gid;
+	}
+	d.mode = mode;
+	if(b.flush() == Bufio->ERROR)
+		warn(sys->sprint("error writing %s: %r", name));
+	if(sys->fwstat(b.fd, d) < 0)
+		warn(sys->sprint("can't set modes for %s: %r", name));
+	if(uflag){
+		(i, d) = sys->fstat(b.fd);
+		if(i < 0)
+			warn(sys->sprint("can't reread modes for %s: %r", name));
+		if(d.mtime != mtime)
+			warn(sys->sprint("%s: time mismatch %ud %ud\n", name, mtime, d.mtime));
+		if(d.uid != uid)
+			warn(sys->sprint("%s: uid mismatch %s %s", name, uid, d.uid));
+		if(d.gid != gid)
+			warn(sys->sprint("%s: gid mismatch %s %s", name, gid, d.gid));
+	}
+	b.close();
+}
+
+seekpast(bytes: big)
+{
+	n: int;
+
+	buf := array [LEN] of byte;
+	for(tot := big 0; tot < bytes; tot += big n){
+		n = len buf;
+		if(tot + big n > bytes)
+			n = int(bytes - tot);
+		n = bin.read(buf, n);
+		if(n <= 0)
+			error("premature eof");
+	}
+}
+
+error(s: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, s);
+	quit("error");
+}
+
+warn(s: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, s);
+}
+
+octal(i: int): string
+{
+	s := "";
+	do {
+		t: string;
+		t[0] = '0' + (i&7);
+		s = t+s;
+	} while((i = (i>>3)&~(7<<29)) != 0);
+	return s;
+}
+
+parent(name : string) : string
+{
+	slash := -1;
+	for (i := 0; i < len name; i++)
+		if (name[i] == '/')
+			slash = i;
+	if (slash > 0)
+		return name[0:slash];
+	return "/";
+}
+
+create(name : string, rw : int, mode : int) : ref Sys->FD
+{
+	fd := sys->create(name, rw, mode);
+	if (fd == nil) {
+		p := parent(name);
+		(ok, d) := sys->stat(p);
+		if (ok < 0)
+			return nil;
+		omode := d.mode;
+		d = sys->nulldir;
+		d.mode = omode | 8r222;		# ensure parent is writable
+		if(sys->wstat(p, d) < 0) {
+			warn(sys->sprint("can't set modes for %s: %r", p));
+			return nil;
+		}
+		fd = sys->create(name, rw, mode);
+		d.mode = omode;
+		sys->wstat(p, d);
+	}
+	return fd;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/mkfile	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,25 @@
+<../../../mkconfig
+
+DIRS=\
+	prep\
+
+TARG=\
+	kfs.dis\
+	mbr.dis\
+	mkext.dis\
+	mkfs.dis\
+	kfscmd.dis\
+	format.dis\
+	ftl.dis\
+
+SYSMODULES=\
+	arg.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	string.m\
+
+DISBIN=$ROOT/dis/disk
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/mkfs.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,732 @@
+implement Mkfs;
+
+include "sys.m";
+	sys: Sys;
+	Dir, sprint, fprint: import sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+	arg: Arg;
+
+Mkfs: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+LEN: con Sys->ATOMICIO;
+HUNKS: con 128;
+
+Kfs, Fs, Archive: con iota;	# types of destination file sytems
+
+File: adt {
+	new:	string;
+	elem:	string;
+	old:	string;
+	uid:	string;
+	gid:	string;
+	mode:	int;
+};
+
+b: ref Iobuf;
+bout: ref Iobuf;			# stdout when writing archive
+newfile: string;
+oldfile: string;
+proto: string;
+cputype: string;
+users: string;
+oldroot: string;
+newroot: string;
+prog := "mkfs";
+lineno := 0;
+buf: array of byte;
+zbuf: array of byte;
+buflen := 1024-8;
+indent: int;
+verb: int;
+modes: int;
+ream: int;
+debug: int;
+xflag: int;
+sfd: ref Sys->FD;
+fskind: int;	# Kfs, Fs, Archive
+user: string;
+stderr: ref Sys->FD;
+usrid, grpid : string;
+setuid: int;
+	
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	arg = load Arg Arg->PATH;
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->FORKFD, nil);
+
+	stderr = sys->fildes(2);
+	if(arg == nil)
+		error(sys->sprint("can't load %q: %r", Arg->PATH));
+
+	user = getuser();
+	if(user == nil)
+		user = "none";
+	name := "";
+	file := ref File;
+	file.new = "";
+	file.old = nil;
+	file.mode = 0;
+	oldroot = "";
+	newroot = "/n/kfs";
+	users = nil;
+	fskind = Kfs;	# i suspect Inferno default should be different
+	arg->init(args);
+	arg->setusage("mkfs [-aprvxS] [-d root] [-n kfscmdname] [-s src-fs] [-u userfile] [-z n] [-G group] [-U user] proto ...");
+	while((c := arg->opt()) != 0)
+		case c {
+		'a' =>
+			fskind = Archive;
+			newroot = "";
+			bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+			if(bout == nil)
+				error(sys->sprint("can't open standard output for archive: %r"));
+		'd' =>
+			fskind = Fs;
+			newroot = arg->earg();
+		'D' =>
+			debug = 1;
+		'n' =>
+			name = arg->earg();
+		'p' =>
+			modes = 1;
+		'q' =>
+			;
+		'r' =>
+			ream = 1;
+		's' =>
+			oldroot = arg->earg();
+		'u' =>
+			users = arg->earg();
+		'v' =>
+			verb = 1;
+		'x' =>
+			xflag = 1;
+		'z' =>
+			(buflen, nil) = str->toint(arg->earg(), 10);
+			buflen -= 8;	# qid.path and tag at end of each kfs block
+		'U' => 
+			usrid = arg->earg();
+		'G' =>
+			grpid = arg->earg();
+		'S' =>
+			setuid = 1;
+		* =>
+			arg->usage();
+		}
+
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+
+	buf = array [buflen] of byte;
+	zbuf = array [buflen] of { * => byte 0 };
+
+	if(name != nil)
+		openkfscmd(name);
+	kfscmd("allow");
+	if(users != nil){
+		proto = "users";	# for diagnostics
+		setusers();
+	}
+	cputype = getenv("cputype");
+	if(cputype == nil)
+		cputype = "dis";
+
+	errs := 0;
+	for(; args != nil; args = tl args){
+		proto = hd args;
+		fprint(stderr, "processing %s\n", proto);
+
+		b = bufio->open(proto, Sys->OREAD);
+		if(b == nil){
+			fprint(stderr, "%s: can't open %q: %r: skipping\n", prog, proto);
+			errs++;
+			continue;
+		}
+
+		lineno = 0;
+		indent = 0;
+		mkfs(file, -1);
+		b.close();
+	}
+	fprint(stderr, "file system made\n");
+	kfscmd("disallow");
+	kfscmd("sync");
+	if(errs)
+		quit("skipped protos");
+	if(fskind == Archive){
+		bout.puts("end of archive\n");
+		if(bout.flush() == Bufio->ERROR)
+			error(sys->sprint("write error: %r"));
+	}
+}
+
+quit(why: string)
+{
+	if(bout != nil)
+		bout.flush();
+	if(why != nil)
+		raise "fail:"+why;
+	exit;
+}
+
+mkfs(me: ref File, level: int)
+{
+	(child, fp) := getfile(me);
+	if(child == nil)
+		return;
+	if(child.elem == "+" || child.elem == "*" || child.elem == "%"){
+		rec := child.elem[0] == '+';
+		filesonly := child.elem[0] == '%';
+		child.new = me.new;
+		setnames(child);
+		mktree(child, rec, filesonly);
+		(child, fp) = getfile(me);
+	}
+	while(child != nil && indent > level){
+		if(mkfile(child))
+			mkfs(child, indent);
+		(child, fp) = getfile(me);
+	}
+	if(child != nil){
+		b.seek(fp, 0);
+		lineno--;
+	}
+}
+
+mktree(me: ref File, rec: int, filesonly: int)
+{
+	fd := sys->open(oldfile, Sys->OREAD);
+	if(fd == nil){
+		warn(sys->sprint("can't open %q: %r", oldfile));
+		return;
+	}
+
+	child := ref *me;
+	r := ref Rec(nil, 0);
+	for(;;){
+		(n, d) := sys->dirread(fd);
+		if(n <= 0)
+			break;
+		for(i := 0; i < n; i++)
+		  	if (!recall(d[i].name, r)) {
+				if(filesonly && d[i].mode & Sys->DMDIR)
+					continue;
+				child.new = mkpath(me.new, d[i].name);
+				if(me.old != nil)
+					child.old = mkpath(me.old, d[i].name);
+				child.elem = d[i].name;
+				setnames(child);
+				if(copyfile(child, ref d[i], 1) && rec)
+					mktree(child, rec, filesonly);
+		  	}
+	}
+}
+
+# Recall namespace fix
+# -- remove duplicates (could use Readdir->init(,Readdir->COMPACT))
+# obc
+
+Rec: adt
+{
+	ad: array of string;
+	l: int;
+};
+
+AL : con HUNKS;
+recall(e : string, r : ref Rec) : int
+{
+	if (r.ad == nil) r.ad = array[AL] of string;
+	# double array
+	if (r.l >= len r.ad) {
+		nar := array[2*(len r.ad)] of string;
+		nar[0:] = r.ad;
+		r.ad = nar;
+	}
+	for(i := 0; i < r.l; i++)
+		if (r.ad[i] == e) return 1;
+	r.ad[r.l++] = e;
+	return 0;
+}
+
+mkfile(f: ref File): int
+{
+	(i, dir) := sys->stat(oldfile);
+	if(i < 0){
+		warn(sys->sprint("can't stat file %q: %r", oldfile));
+		skipdir();
+		return 0;
+	}
+	return copyfile(f, ref dir, 0);
+}
+
+copyfile(f: ref File, d: ref Dir, permonly: int): int
+{
+	mode: int;
+
+	if(xflag && bout != nil){
+		bout.puts(sys->sprint("%q\t%d\t%bd\n", f.new, d.mtime, d.length));
+		return (d.mode & Sys->DMDIR) != 0;
+	}
+	d.name = f.elem;
+	if(d.dtype != 'M' && d.dtype != 'U'){		# hmm... Indeed!
+		d.uid = "inferno";
+		d.gid = "inferno";
+		mode = (d.mode >> 6) & 7;
+		d.mode |= mode | (mode << 3);
+	}
+	if(f.uid != "-")
+		d.uid = f.uid;
+	if(f.gid != "-")
+		d.gid = f.gid;
+	if(fskind == Fs && !setuid){	# new system: set to nil
+		d.uid = user;
+		d.gid = user;
+	}
+	if (usrid != nil)
+		d.uid = usrid;
+	if (grpid != nil)
+		d.gid = grpid;
+	if(f.mode != ~0){
+		if(permonly)
+			d.mode = (d.mode & ~8r666) | (f.mode & 8r666);
+		else if((d.mode&Sys->DMDIR) != (f.mode&Sys->DMDIR))
+			warn(sys->sprint("inconsistent mode for %s", f.new));
+		else
+			d.mode = f.mode;
+	}
+	if(!uptodate(d, newfile)){
+		if(d.mode & Sys->DMDIR)
+			mkdir(d);
+		else {
+			if(verb)
+				fprint(stderr, "%q\n", f.new);
+			copy(d);
+		}
+	}else if(modes){
+		nd := sys->nulldir;
+		nd.mode = d.mode;
+		nd.mtime = d.mtime;
+		nd.gid = d.gid;
+		if(sys->wstat(newfile, nd) < 0)
+			warn(sys->sprint("can't set modes for %q: %r", f.new));
+		# do the uid separately since different file systems object
+		nd = sys->nulldir;
+		nd.uid = d.uid;
+		sys->wstat(newfile, nd);
+	}
+	return (d.mode & Sys->DMDIR) != 0;
+}
+
+
+# check if file to is up to date with
+# respect to the file represented by df
+
+uptodate(df: ref Dir, newf: string): int
+{
+	if(fskind == Archive || ream)
+		return 0;
+	(i, dt) := sys->stat(newf);
+	if(i < 0)
+		return 0;
+	return dt.mtime >= df.mtime;
+}
+
+copy(d: ref Dir)
+{
+	t: ref Sys->FD;
+	n: int;
+
+	f := sys->open(oldfile, Sys->OREAD);
+	if(f == nil){
+		warn(sys->sprint("can't open %q: %r", oldfile));
+		return;
+	}
+	t = nil;
+	if(fskind == Archive)
+		arch(d);
+	else{
+		(dname, fname) := str->splitr(newfile, "/");
+		if(fname == nil)
+			error(sys->sprint("internal temporary file error (%s)", dname));
+		cptmp := dname+"__mkfstmp";
+		t = sys->create(cptmp, Sys->OWRITE, 8r666);
+		if(t == nil){
+			warn(sys->sprint("can't create %q: %r", newfile));
+			return;
+		}
+	}
+
+	for(tot := big 0;; tot += big n){
+		n = sys->read(f, buf, buflen);
+		if(n < 0){
+			warn(sys->sprint("can't read %q: %r", oldfile));
+			break;
+		}
+		if(n == 0)
+			break;
+		if(fskind == Archive){
+			if(bout.write(buf, n) != n)
+				error(sys->sprint("write error: %r"));
+		}else if(buf[0:buflen] == zbuf[0:buflen]){
+			if(sys->seek(t, big buflen, 1) < big 0)
+				error(sys->sprint("can't write zeros to %q: %r", newfile));
+		}else if(sys->write(t, buf, n) < n)
+			error(sys->sprint("can't write %q: %r", newfile));
+	}
+	f = nil;
+	if(tot != d.length){
+		warn(sys->sprint("wrong number bytes written to %s (was %bd should be %bd)",
+			newfile, tot, d.length));
+		if(fskind == Archive){
+			warn("seeking to proper position");
+			bout.seek(d.length - tot, 1);
+		}
+	}
+	if(fskind == Archive)
+		return;
+	sys->remove(newfile);
+	nd := sys->nulldir;
+	nd.name = d.name;
+	nd.mode = d.mode;
+	nd.mtime = d.mtime;
+	if(sys->fwstat(t, nd) < 0)
+		error(sys->sprint("can't move tmp file to %q: %r", newfile));
+	nd = sys->nulldir;
+	nd.gid = d.gid;
+	if(sys->fwstat(t, nd) < 0)
+		warn(sys->sprint("can't set group id of %q to %q: %r", newfile, d.gid));
+	nd.gid = nil;
+	nd.uid = d.uid;
+	sys->fwstat(t, nd);
+}
+
+mkdir(d: ref Dir)
+{
+	if(fskind == Archive){
+		arch(d);
+		return;
+	}
+	fd := sys->create(newfile, Sys->OREAD, d.mode);
+	nd := sys->nulldir;
+	nd.mode = d.mode;
+	nd.gid = d.gid;
+	nd.mtime = d.mtime;
+	if(fd == nil){
+		(i, d1) := sys->stat(newfile);
+		if(i < 0 || !(d1.mode & Sys->DMDIR))
+			error(sys->sprint("can't create %q", newfile));
+		if(sys->wstat(newfile, nd) < 0)
+			warn(sys->sprint("can't set modes for %q: %r", newfile));
+		nd = sys->nulldir;
+		nd.uid = d.uid;
+		sys->wstat(newfile, nd);
+		return;
+	}
+	if(sys->fwstat(fd, nd) < 0)
+		warn(sys->sprint("can't set modes for %q: %r", newfile));
+	nd = sys->nulldir;
+	nd.uid = d.uid;
+	sys->fwstat(fd, nd);
+}
+
+arch(d: ref Dir)
+{
+	bout.puts(sys->sprint("%q %uo %q %q %ud %bd\n",
+		newfile, d.mode, d.uid, d.gid, d.mtime, d.length));
+}
+
+mkpath(prefix, elem: string): string
+{
+	return sys->sprint("%s/%s", prefix, elem);
+}
+
+setnames(f: ref File)
+{
+	newfile = newroot+f.new;
+	if(f.old != nil){
+		if(f.old[0] == '/')
+			oldfile = oldroot+f.old;
+		else
+			oldfile = f.old;
+	}else
+		oldfile = oldroot+f.new;
+}
+
+#
+# skip all files in the proto that
+# could be in the current dir
+#
+skipdir()
+{
+	if(indent < 0)
+		return;
+	level := indent;
+	for(;;){
+		indent = 0;
+		fp := b.offset();
+		p := b.gets('\n');
+		lineno++;
+		if(p == nil){
+			indent = -1;
+			return;
+		}
+		for(j := 0; (c := p[j++]) != '\n';)
+			if(c == ' ')
+				indent++;
+			else if(c == '\t')
+				indent += 8;
+			else
+				break;
+		if(indent <= level){
+			b.seek(fp, 0);
+			lineno--;
+			return;
+		}
+	}
+}
+
+getfile(old: ref File): (ref File, big)
+{
+	f: ref File;
+	p, elem: string;
+	c: int;
+
+	if(indent < 0)
+		return (nil, big 0);
+	fp := b.offset();
+	do {
+		indent = 0;
+		p = b.gets('\n');
+		lineno++;
+		if(p == nil){
+			indent = -1;
+			return (nil, big 0);
+		}
+		for(; (c = p[0]) != '\n'; p = p[1:])
+			if(c == ' ')
+				indent++;
+			else if(c == '\t')
+				indent += 8;
+			else
+				break;
+	} while(c == '\n' || c == '#');
+	f = ref File;
+	(elem, p) = getname(p);
+	if(debug)
+		fprint(stderr, "getfile: %q root %q\n", elem, old.new);
+	f.new = mkpath(old.new, elem);
+	(nil, f.elem) = str->splitr(f.new, "/");
+	if(f.elem == nil)
+		error(sys->sprint("can't find file name component of %q", f.new));
+	(f.mode, p) = getmode(p);
+	(f.uid, p) = getname(p);
+	if(f.uid == nil)
+		f.uid = "-";
+	(f.gid, p) = getname(p);
+	if(f.gid == nil)
+		f.gid = "-";
+	f.old = getpath(p);
+	if(f.old == "-")
+		f.old = nil;
+	setnames(f);
+
+	if(debug)
+		printfile(f);
+
+	return (f, fp);
+}
+
+getpath(p: string): string
+{
+	for(i := 0; i < len p && (p[i] == ' ' || p[i] == '\t'); i++)
+		;
+	for(n := i; n < len p && (c := p[n]) != '\n' && c != ' ' && c != '\t'; n++)
+		;
+	return p[i:n];
+}
+
+getname(p: string): (string, string)
+{
+	for(i := 0; i < len p && (p[0] == ' ' || p[0] == '\t'); i++)
+		;
+	s := "";
+	quoted := 0;
+	for(; i < len p && (c := p[i]) != '\n' && (c != ' ' && c != '\t' || quoted); i++){
+		if(c == '\''){
+			if(i+1 >= len p || p[i+1] != '\''){
+				quoted = !quoted;
+				continue;
+			}
+			i++;
+		}
+		s[len s] = c;
+	}
+	if(len s > 0 && s[0] == '$'){
+		s = getenv(s[1:]);
+		if(s == nil)
+			error(sys->sprint("can't read environment variable %q", s));
+	}
+	return (s, p[i:]);
+}
+
+getenv(s: string): string
+{
+	if(s == "user")
+		return getuser();
+	return readfile("/env/"+s);
+}
+
+getuser(): string
+{
+	return readfile("/dev/user");
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd != nil){
+		a := array[256] of byte;
+		n := sys->read(fd, a, len a);
+		if(n > 0)
+			return string a[0:n];
+	}
+	return nil;
+}
+
+getmode(p: string): (int, string)
+{
+	s: string;
+
+	(s, p) = getname(p);
+	if(s == nil || s == "-")
+		return (~0, p);
+	os := s;
+	m := 0;
+	if(s[0] == 'd'){
+		m |= Sys->DMDIR;
+		s = s[1:];
+	}
+	if(s[0] == 'a'){
+		m |= Sys->DMAPPEND;
+		s = s[1:];
+	}
+	if(s[0] == 'l'){
+		m |= Sys->DMEXCL;
+		s = s[1:];
+	}
+
+	for(i:=0; i<len s || i < 3; i++)
+		if(i >= len s || !(s[i]>='0' && s[i]<='7')){
+			warn(sys->sprint("bad mode specification %s", os));
+			return (~0, p);
+		}
+	(v, nil) := str->toint(s, 8);
+	return (m|v, p);
+}
+
+setusers()
+{
+	if(fskind != Kfs)
+		return;
+	file := ref File;
+	m := modes;
+	modes = 1;
+	file.uid = "adm";
+	file.gid = "adm";
+	file.mode = Sys->DMDIR|8r775;
+	file.new = "/adm";
+	file.elem = "adm";
+	file.old = nil;
+	setnames(file);
+	mkfile(file);
+	file.new = "/adm/users";
+	file.old = users;
+	file.elem = "users";
+	file.mode = 8r664;
+	setnames(file);
+	mkfile(file);
+	kfscmd("user");
+	mkfile(file);
+	file.mode = Sys->DMDIR|8r775;
+	file.new = "/adm";
+	file.old = "/adm";
+	file.elem = "adm";
+	setnames(file);
+	mkfile(file);
+	modes = m;
+}
+
+openkfscmd(name: string)
+{
+	if(fskind != Kfs)
+		return;
+	kname := sys->sprint("/chan/kfs.%s.cmd", name);
+	sfd = sys->open(kname, Sys->ORDWR);
+	if(sfd == nil){
+		fprint(stderr, "%s: can't open %q: %r\n", prog, kname);
+		quit("open kfscmd");
+	}
+}
+
+kfscmd(cmd: string)
+{
+	if(fskind != Kfs || sfd == nil)
+		return;
+	a := array of byte cmd;
+	if(sys->write(sfd, a, len a) != len a){
+		fprint(stderr, "%s: error writing %s: %r", prog, cmd);
+		return;
+	}
+	for(;;){
+		reply := array[4*1024] of byte;
+		n := sys->read(sfd, reply, len reply);
+		if(n <= 0)
+			return;
+		s := string reply[0:n];
+		if(s == "done" || s == "success")
+			return;
+		if(s == "unknown command"){
+			fprint(stderr, "%s: command %s not recognized\n", prog, cmd);
+			return;
+		}
+	}
+}
+
+error(s: string)
+{
+	fprint(stderr, "%s: %s:%d: %s\n", prog, proto, lineno, s);
+	kfscmd("disallow");
+	kfscmd("sync");
+	quit("error");
+}
+
+warn(s: string)
+{
+	fprint(stderr, "%s: %s:%d: %s\n", prog, proto, lineno, s);
+}
+
+printfile(f: ref File)
+{
+	if(f.old != nil)
+		fprint(stderr, "%q from %q %q %q %uo\n", f.new, f.old, f.uid, f.gid, f.mode);
+	else
+		fprint(stderr, "%q %q %q %uo\n", f.new, f.uid, f.gid, f.mode);
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/calc.tab.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,454 @@
+implement Calc;
+
+#line	2	"calc.y"
+#
+# from Plan 9.  subject to the Lucent Public License 1.02
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+	NUM,
+	DOT,
+	DOLLAR,
+	ADD,
+	SUB,
+	MUL,
+	DIV,
+	FRAC,
+	NEG: con iota;
+
+Exp: adt {
+	ty:	int;
+	n:	big;
+	e1, e2:	cyclic ref Exp;
+};
+
+YYSTYPE: adt {
+	e:	ref Exp;
+};
+yyexp: ref Exp;
+
+YYLEX: adt {
+	s:	string;
+	n:	int;
+	lval: YYSTYPE;
+	lex: fn(l: self ref YYLEX): int;
+	error: fn(l: self ref YYLEX, msg: string);
+};
+Calc: module {
+
+	parseexpr: fn(s: string, a, b, c: big): (big, string);
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+NUMBER: con	57346;
+UNARYMINUS: con	57347;
+
+};
+YYEOFCODE: con 1;
+YYERRCODE: con 2;
+YYMAXDEPTH: con 200;
+
+#line	68	"calc.y"
+
+
+mkNUM(x: big): ref Exp
+{
+	return ref Exp(NUM, x, nil, nil);
+}
+
+mkOP(ty: int, e1: ref Exp, e2: ref Exp): ref Exp
+{
+	return ref Exp(ty, big 0, e1, e2);
+}
+
+dot, size, dollar: big;
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	while(l.n < len l.s && isspace(l.s[l.n]))
+		l.n++;
+
+	if(l.n == len l.s)
+		return -1;
+
+	if(isdigit(l.s[l.n])){
+		for(o := l.n; o < len l.s && isdigit(l.s[o]); o++)
+			;
+		l.lval.e = mkNUM(big l.s[l.n:o]);
+		l.n = o;
+		return NUMBER;
+	}
+
+	return l.s[l.n++];
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
+}
+
+YYLEX.error(nil: self ref YYLEX, s: string)
+{
+	raise s;
+}
+
+eval(e: ref Exp): big
+{
+	case e.ty {
+	NUM =>
+		return e.n;
+	DOT =>
+		return dot;
+	DOLLAR =>
+		return dollar;
+	ADD =>
+		return eval(e.e1)+eval(e.e2);
+	SUB =>
+		return eval(e.e1)-eval(e.e2);
+	MUL =>
+		return eval(e.e1)*eval(e.e2);
+	DIV =>
+		i := eval(e.e2);
+		if(i == big 0)
+			raise "division by zero";
+		return eval(e.e1)/i;
+	FRAC =>
+		return (size*eval(e.e1))/big 100;
+	NEG =>
+		return -eval(e.e1);
+	* =>
+		raise "invalid operator";
+	}
+}
+
+parseexpr(s: string, xdot: big, xdollar: big, xsize: big): (big, string)
+{
+	dot = xdot;
+	size = xsize;
+	dollar = xdollar;
+	l := ref YYLEX(s, 0, YYSTYPE(nil));
+	{
+		yyparse(l);
+		if(yyexp == nil)
+			return (big 0, "nil yylval?");
+		return (eval(yyexp), nil);
+	}exception e{
+	"*" =>
+		return (big 0, e);
+	}
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	while((args = tl args) != nil){
+		(r, e) := parseexpr(hd args, big 1000, big 1000000, big 1000000);
+		if(e != nil)
+			sys->print("%s\n", e);
+		else
+			sys->print("%bd\n", r);
+	}
+}
+
+yyexca := array[] of {-1, 1,
+	1, -1,
+	-2, 0,
+};
+YYNPROD: con 12;
+YYPRIVATE: con 57344;
+yytoknames: array of string;
+yystates: array of string;
+yydebug: con 0;
+YYLAST:	con 30;
+yyact := array[] of {
+   8,   9,  10,  11,   3,  12,   7,   2,  12,  19,
+   1,   4,   5,   6,  13,  14,  15,  16,  17,  18,
+   8,   9,  10,  11,   0,  12,  10,  11,   0,  12,
+};
+yypact := array[] of {
+   0,-1000,  15,-1000,-1000,-1000,   0,   0,   0,   0,
+   0,   0,-1000,  -5,-1000,  19,  19,  -2,  -2,-1000,
+};
+yypgo := array[] of {
+   0,   7,  10,
+};
+yyr1 := array[] of {
+   0,   2,   1,   1,   1,   1,   1,   1,   1,   1,
+   1,   1,
+};
+yyr2 := array[] of {
+   0,   1,   1,   1,   1,   3,   3,   3,   3,   3,
+   2,   2,
+};
+yychk := array[] of {
+-1000,  -2,  -1,   4,  11,  12,  13,   6,   5,   6,
+   7,   8,  10,  -1,  -1,  -1,  -1,  -1,  -1,  14,
+};
+yydef := array[] of {
+   0,  -2,   1,   2,   3,   4,   0,   0,   0,   0,
+   0,   0,  10,   0,  11,   6,   7,   8,   9,   5,
+};
+yytok1 := array[] of {
+   1,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,  12,  10,   3,   3,
+  13,  14,   7,   5,   3,   6,  11,   8,
+};
+yytok2 := array[] of {
+   2,   3,   4,   9,
+};
+yytok3 := array[] of {
+   0
+};
+
+YYSys: module
+{
+	FD: adt
+	{
+		fd:	int;
+	};
+	fildes:		fn(fd: int): ref FD;
+	fprint:		fn(fd: ref FD, s: string, *): int;
+};
+
+yysys: YYSys;
+yystderr: ref YYSys->FD;
+
+YYFLAG: con -1000;
+
+# parser for yacc output
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(yylex: ref YYLEX): int
+{
+	c : int;
+	yychar := yylex.lex();
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		yysys->fprint(yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(yylex: ref YYLEX): int
+{
+	if(yydebug >= 1 && yysys == nil) {
+		yysys = load YYSys "$Sys";
+		yystderr = yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yyval: YYSTYPE;
+	yystate := 0;
+	yychar := -1;
+	yynerrs := 0;		# number of errors
+	yyerrflag := 0;		# error recovery flag
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= len yys)
+			yys = (array[len yys * 2] of YYS)[0:] = yys;
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= len yys)
+							yys = (array[len yys * 2] of YYS)[0:] = yys;
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = yylex.lval;
+						if(yyerrflag > 0)
+							yyerrflag--;
+						if(yydebug >= 4)
+							yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(yyerrflag == 0) { # brand new error
+				yylex.error("syntax error");
+				yynerrs++;
+				if(yydebug >= 1) {
+					yysys->fprint(yystderr, "%s", yystatname(yystate));
+					yysys->fprint(yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(yyerrflag != 3) { # incompletely recovered error ... try again
+				yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE)
+							continue yystack;
+					}
+	
+					# the current yyp has no shift onn "error", pop stack
+					if(yydebug >= 2)
+						yysys->fprint(yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				yysys->fprint(yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			yysys->fprint(yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+#		yyval = yys[yyp+1].yyv;
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			
+1=>
+#line	54	"calc.y"
+{ yyexp = yys[yypt-0].yyv.e; return 0; }
+2=>
+yyval.e = yys[yyp+1].yyv.e;
+3=>
+#line	57	"calc.y"
+{ yyval.e = mkOP(DOT, nil, nil); }
+4=>
+#line	58	"calc.y"
+{ yyval.e = mkOP(DOLLAR, nil, nil); }
+5=>
+#line	59	"calc.y"
+{ yyval.e = yys[yypt-1].yyv.e; }
+6=>
+#line	60	"calc.y"
+{ yyval.e = mkOP(ADD, yys[yypt-2].yyv.e, yys[yypt-0].yyv.e); }
+7=>
+#line	61	"calc.y"
+{ yyval.e = mkOP(SUB, yys[yypt-2].yyv.e, yys[yypt-0].yyv.e); }
+8=>
+#line	62	"calc.y"
+{ yyval.e = mkOP(MUL, yys[yypt-2].yyv.e, yys[yypt-0].yyv.e); }
+9=>
+#line	63	"calc.y"
+{ yyval.e = mkOP(DIV, yys[yypt-2].yyv.e, yys[yypt-0].yyv.e); }
+10=>
+#line	64	"calc.y"
+{ yyval.e = mkOP(FRAC, yys[yypt-1].yyv.e, nil); }
+11=>
+#line	65	"calc.y"
+{ yyval.e = mkOP(NEG, yys[yypt-0].yyv.e, nil); }
+		}
+	}
+
+	return yyn;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/calc.tab.m	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,7 @@
+Calc: module {
+
+	parseexpr: fn(s: string, a, b, c: big): (big, string);
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+NUMBER: con	57346;
+UNARYMINUS: con	57347;
+};
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/calc.y	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,174 @@
+%{
+#
+# from Plan 9.  subject to the Lucent Public License 1.02
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+	NUM,
+	DOT,
+	DOLLAR,
+	ADD,
+	SUB,
+	MUL,
+	DIV,
+	FRAC,
+	NEG: con iota;
+
+Exp: adt {
+	ty:	int;
+	n:	big;
+	e1, e2:	cyclic ref Exp;
+};
+
+YYSTYPE: adt {
+	e:	ref Exp;
+};
+yyexp: ref Exp;
+
+YYLEX: adt {
+	s:	string;
+	n:	int;
+	lval: YYSTYPE;
+	lex: fn(l: self ref YYLEX): int;
+	error: fn(l: self ref YYLEX, msg: string);
+};
+%}
+%module Calc
+{
+	parseexpr: fn(s: string, a, b, c: big): (big, string);
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+}
+
+%token <e> NUMBER
+
+%type <e> expr
+
+%left '+' '-'
+%left '*' '/'
+%left UNARYMINUS '%'
+%%
+top:	expr	{ yyexp = $1; return 0; }
+
+expr:	NUMBER
+	| '.'	{ $$ = mkOP(DOT, nil, nil); }
+	| '$'	{ $$ = mkOP(DOLLAR, nil, nil); }
+	| '(' expr ')'	{ $$ = $2; }
+	| expr '+' expr	{ $$ = mkOP(ADD, $1, $3); }
+	| expr '-' expr 	{ $$ = mkOP(SUB, $1, $3); }
+	| expr '*' expr	{ $$ = mkOP(MUL, $1, $3); }
+	| expr '/' expr	{ $$ = mkOP(DIV, $1, $3); }
+	| expr '%'		{ $$ = mkOP(FRAC, $1, nil); }
+	| '-' expr %prec UNARYMINUS	{ $$ = mkOP(NEG, $2, nil); }
+	;
+
+%%
+
+mkNUM(x: big): ref Exp
+{
+	return ref Exp(NUM, x, nil, nil);
+}
+
+mkOP(ty: int, e1: ref Exp, e2: ref Exp): ref Exp
+{
+	return ref Exp(ty, big 0, e1, e2);
+}
+
+dot, size, dollar: big;
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	while(l.n < len l.s && isspace(l.s[l.n]))
+		l.n++;
+
+	if(l.n == len l.s)
+		return -1;
+
+	if(isdigit(l.s[l.n])){
+		for(o := l.n; o < len l.s && isdigit(l.s[o]); o++)
+			;
+		l.lval.e = mkNUM(big l.s[l.n:o]);
+		l.n = o;
+		return NUMBER;
+	}
+
+	return l.s[l.n++];
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
+}
+
+YYLEX.error(nil: self ref YYLEX, s: string)
+{
+	raise s;
+}
+
+eval(e: ref Exp): big
+{
+	case e.ty {
+	NUM =>
+		return e.n;
+	DOT =>
+		return dot;
+	DOLLAR =>
+		return dollar;
+	ADD =>
+		return eval(e.e1)+eval(e.e2);
+	SUB =>
+		return eval(e.e1)-eval(e.e2);
+	MUL =>
+		return eval(e.e1)*eval(e.e2);
+	DIV =>
+		i := eval(e.e2);
+		if(i == big 0)
+			raise "division by zero";
+		return eval(e.e1)/i;
+	FRAC =>
+		return (size*eval(e.e1))/big 100;
+	NEG =>
+		return -eval(e.e1);
+	* =>
+		raise "invalid operator";
+	}
+}
+
+parseexpr(s: string, xdot: big, xdollar: big, xsize: big): (big, string)
+{
+	dot = xdot;
+	size = xsize;
+	dollar = xdollar;
+	l := ref YYLEX(s, 0, YYSTYPE(nil));
+	{
+		yyparse(l);
+		if(yyexp == nil)
+			return (big 0, "nil yylval?");
+		return (eval(yyexp), nil);
+	}exception e{
+	"*" =>
+		return (big 0, e);
+	}
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	while((args = tl args) != nil){
+		(r, e) := parseexpr(hd args, big 1000, big 1000000, big 1000000);
+		if(e != nil)
+			sys->print("%s\n", e);
+		else
+			sys->print("%bd\n", r);
+	}
+}
+
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/fdisk.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,924 @@
+implement Fdisk;
+
+#
+# fdisk - edit dos disk partition table
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "disks.m";
+	disks: Disks;
+	Disk, PCpart: import disks;
+	NTentry, Toffset, TentrySize: import Disks;
+	Magic0, Magic1: import Disks;
+
+include "pedit.m";
+	pedit: Pedit;
+	Edit, Part: import pedit;
+
+include "arg.m";
+
+Fdisk: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Mpart: con 64;
+
+blank := 0;
+dowrite := 0;
+file := 0;
+rdonly := 0;
+doauto := 0;
+mbroffset := big 0;
+printflag := 0;
+printchs := 0;
+sec2cyl := big 0;
+written := 0;
+
+edit: ref Edit;
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	disks = load Disks Disks->PATH;
+	pedit = load Pedit Pedit->PATH;
+
+	sys->pctl(Sys->FORKFD, nil);
+	disks->init();
+	pedit->init();
+
+	edit = Edit.mk("cylinder");
+
+	edit.add = cmdadd;
+	edit.del = cmddel;
+	edit.okname = cmdokname;
+	edit.ext = cmdext;
+	edit.help = cmdhelp;
+	edit.sum = cmdsum;
+	edit.write = cmdwrite;
+	edit.printctl = cmdprintctl;
+
+	stderr = sys->fildes(2);
+
+	secsize := 0;
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("disk/fdisk [-abfprvw] [-s sectorsize] /dev/sdC0/data");
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>
+			doauto++;
+		'b' =>
+			blank++;
+		'f' =>
+			file++;
+		'p' =>
+			printflag++;
+		'r' =>
+			rdonly++;
+		's' =>
+			secsize = int arg->earg();
+		'v' =>
+			printchs++;
+		'w' =>
+			dowrite++;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	arg = nil;
+
+	mode := Sys->ORDWR;
+	if(rdonly)
+		mode = Sys->OREAD;
+	edit.disk = Disk.open(hd args, mode, file);
+	if(edit.disk == nil) {
+		sys->fprint(stderr, "cannot open disk: %r\n");
+		exits("opendisk");
+	}
+
+	if(secsize != 0) {
+		edit.disk.secsize = secsize;
+		edit.disk.secs = edit.disk.size / big secsize;
+	}
+
+	sec2cyl = big (edit.disk.h * edit.disk.s);
+	edit.end = edit.disk.secs / sec2cyl;
+
+	findmbr(edit);
+
+	if(blank)
+		blankpart(edit);
+	else
+		rdpart(edit, big 0, big 0);
+
+	if(doauto)
+		autopart(edit);
+
+	{
+		if(dowrite)
+			edit.runcmd("w");
+
+		if(printflag)
+			edit.runcmd("P");
+
+		if(dowrite || printflag)
+			exits(nil);
+
+		sys->fprint(stderr, "cylinder = %bd bytes\n", sec2cyl*big edit.disk.secsize);
+		edit.runcmd("p");
+		for(;;) {
+			sys->fprint(stderr, ">>> ");
+			edit.runcmd(edit.getline());
+		}
+	}exception e{
+	"*" =>
+		sys->fprint(stderr, "fdisk: exception %q\n", e);
+		if(written)
+			recover(edit);
+	}
+}
+
+Active:	con 16r80;		# partition is active
+Primary:	con 16r01;		# internal flag
+
+TypeBB:	con 16rFF;
+
+TypeEMPTY:	con 16r00;
+TypeFAT12:	con 16r01;
+TypeXENIX:	con 16r02;		# root
+TypeXENIXUSR:	con 16r03;		# usr
+TypeFAT16:	con 16r04;
+TypeEXTENDED:	con 16r05;
+TypeFATHUGE:	con 16r06;
+TypeHPFS:	con 16r07;
+TypeAIXBOOT:	con 16r08;
+TypeAIXDATA:	con 16r09;
+TypeOS2BOOT:	con 16r0A;		# OS/2 Boot Manager
+TypeFAT32:	con 16r0B;		# FAT 32
+TypeFAT32LBA:	con 16r0C;		# FAT 32 needing LBA support
+TypeEXTHUGE:	con 16r0F;		# FAT 32 extended partition
+TypeUNFORMATTED:	con 16r16;		# unformatted primary partition (OS/2 FDISK)?
+TypeHPFS2:	con 16r17;
+TypeIBMRecovery:	con 16r1C;	# really hidden fat
+TypeCPM0:	con 16r52;
+TypeDMDDO:	con 16r54;		# Disk Manager Dynamic Disk Overlay
+TypeGB:	con 16r56;		# ????
+TypeSPEEDSTOR:	con 16r61;
+TypeSYSV386:	con 16r63;		# also HURD?
+TypeNETWARE:	con 16r64;
+TypePCIX:	con 16r75;
+TypeMINIX13:	con 16r80;		# Minix v1.3 and below
+TypeMINIX:	con 16r81;		# Minix v1.5+
+TypeLINUXSWAP:	con 16r82;
+TypeLINUX:	con 16r83;
+TypeLINUXEXT:	con 16r85;
+TypeAMOEBA:	con 16r93;
+TypeAMOEBABB:	con 16r94;
+TypeBSD386:	con 16rA5;
+TypeBSDI:	con 16rB7;
+TypeBSDISWAP:	con 16rB8;
+TypeOTHER:	con 16rDA;
+TypeCPM:	con 16rDB;
+TypeDellRecovery:	con 16rDE;
+TypeSPEEDSTOR12:	con 16rE1;
+TypeSPEEDSTOR16:	con 16rE4;
+TypeLANSTEP:	con 16rFE;
+
+Type9:	con Disks->Type9;
+
+TableSize: con TentrySize*NTentry;
+Omagic: con TableSize;
+
+Type: adt {
+	desc:	string;
+	name:	string;
+};
+
+Dospart: adt {
+	p:	ref Part;
+	pc:	ref PCpart;
+	primary:	int;
+	lba:	big;	# absolute address
+	size:	big;
+};
+
+Recover: adt {
+	table:	array of byte;	# [TableSize+2] copy of table and magic
+	lba:	big;	# where it came from
+};
+
+types: array of Type = array[256] of {
+	TypeEMPTY =>		( "EMPTY", "" ),
+	TypeFAT12 =>		( "FAT12", "dos" ),
+	TypeFAT16 =>		( "FAT16", "dos" ),
+	TypeFAT32 =>		( "FAT32", "dos" ),
+	TypeFAT32LBA =>		( "FAT32LBA", "dos" ),
+	TypeEXTHUGE =>		( "EXTHUGE", "" ),
+	TypeIBMRecovery =>	( "IBMRECOVERY", "ibm" ),
+	TypeEXTENDED =>		( "EXTENDED", "" ),
+	TypeFATHUGE =>		( "FATHUGE", "dos" ),
+	TypeBB =>		( "BB", "bb" ),
+
+	TypeXENIX =>		( "XENIX", "xenix" ),
+	TypeXENIXUSR =>		( "XENIX USR", "xenixusr" ),
+	TypeHPFS =>		( "HPFS", "ntfs" ),
+	TypeAIXBOOT =>		( "AIXBOOT", "aixboot" ),
+	TypeAIXDATA =>		( "AIXDATA", "aixdata" ),
+	TypeOS2BOOT =>		( "OS/2BOOT", "os2boot" ),
+	TypeUNFORMATTED =>	( "UNFORMATTED", "" ),
+	TypeHPFS2 =>		( "HPFS2", "hpfs2" ),
+	TypeCPM0 =>		( "CPM0", "cpm0" ),
+	TypeDMDDO =>		( "DMDDO", "dmdd0" ),
+	TypeGB =>		( "GB", "gb" ),
+	TypeSPEEDSTOR =>		( "SPEEDSTOR", "speedstor" ),
+	TypeSYSV386 =>		( "SYSV386", "sysv386" ),
+	TypeNETWARE =>		( "NETWARE", "netware" ),
+	TypePCIX =>		( "PCIX", "pcix" ),
+	TypeMINIX13 =>		( "MINIXV1.3", "minix13" ),
+	TypeMINIX =>		( "MINIXV1.5", "minix15" ),
+	TypeLINUXSWAP =>		( "LINUXSWAP", "linuxswap" ),
+	TypeLINUX =>		( "LINUX", "linux" ),
+	TypeLINUXEXT =>		( "LINUXEXTENDED", "" ),
+	TypeAMOEBA =>		( "AMOEBA", "amoeba" ),
+	TypeAMOEBABB =>		( "AMOEBABB", "amoebaboot" ),
+	TypeBSD386 =>		( "BSD386", "bsd386" ),
+	TypeBSDI =>		( "BSDI", "bsdi" ),
+	TypeBSDISWAP =>		( "BSDISWAP", "bsdiswap" ),
+	TypeOTHER =>		( "OTHER", "other" ),
+	TypeCPM =>		( "CPM", "cpm" ),
+	TypeDellRecovery =>	( "DELLRECOVERY", "dell" ),
+	TypeSPEEDSTOR12 =>	( "SPEEDSTOR12", "speedstor" ),
+	TypeSPEEDSTOR16 =>	( "SPEEDSTOR16", "speedstor" ),
+	TypeLANSTEP =>		( "LANSTEP", "lanstep" ),
+
+	Type9 =>			( "PLAN9", "plan9" ),
+
+	* =>	(nil, nil),
+};
+
+dosparts: list of ref Dospart;
+
+tag2part(p: ref Part): ref Dospart
+{
+	for(l := dosparts; l != nil; l = tl l)
+		if((hd l).p.tag == p.tag)
+			return hd l;
+	raise "tag2part: cannot happen";
+}
+
+typestr0(ptype: int): string
+{
+	if(ptype < 0 || ptype >= len types || types[ptype].desc == nil)
+		return sys->sprint("type %d", ptype);
+	return types[ptype].desc;
+}
+
+gettable(disk: ref Disk, addr: big, mbr: int): array of byte
+{
+	table := array[TableSize+2] of {* => byte 0};
+	diskread(disk, table, len table, addr, Toffset);
+	if(mbr){
+		# the informal specs say all must have this but apparently not, only mbr
+		if(int table[Omagic] != Magic0 || int table[Omagic+1] != Magic1)
+			sysfatal("did not find master boot record");
+	}
+	return table;
+}
+
+diskread(disk: ref Disk, data: array of byte, ndata: int, sec: big, off: int)
+{
+	a := sec*big disk.secsize + big off;
+	if(sys->seek(disk.fd, a, 0) != a)
+		sysfatal(sys->sprint("diskread seek %bud.%ud: %r", sec, off));
+	if(sys->readn(disk.fd, data, ndata) != ndata)
+		sysfatal(sys->sprint("diskread %ud at %bud.%ud: %r", ndata, sec, off));
+}
+
+puttable(disk: ref Disk, table: array of byte, sec: big): int
+{
+	return diskwrite(disk, table, len table, sec, Toffset);
+}
+
+diskwrite(disk: ref Disk, data: array of byte, ndata: int, sec: big, off: int): int
+{
+	written = 1;
+	a := sec*big disk.secsize + big off;
+	if(sys->seek(disk.wfd, a, 0) != a ||
+	   sys->write(disk.wfd, data, ndata) != ndata){
+		sys->fprint(stderr, "write %d bytes at %bud.%ud failed: %r\n", ndata, sec, off);
+		return -1;
+	}
+	return 0;
+}
+
+partgen := 0;
+parttag := 0;
+
+mkpart(name: string, primary: int, lba: big, size: big, pcpart: ref PCpart): ref Dospart
+{
+	p := ref Dospart;
+	if(name == nil){
+		if(primary)
+			c := 'p';
+		else
+			c = 's';
+		name = sys->sprint("%c%d", c, ++partgen);
+	}
+
+	if(pcpart != nil)
+		p.pc = pcpart;
+	else
+		p.pc = ref PCpart(0, 0, big 0, big 0, big 0);
+
+	p.primary = primary;
+	p.p = ref Part;	# TO DO
+	p.p.name = name;
+	p.p.start = lba/sec2cyl;
+	p.p.end = (lba+size)/sec2cyl;
+	p.p.ctlstart = lba;
+	p.p.ctlend = lba+size;
+	p.p.tag = ++parttag;
+	p.lba = lba;	# absolute lba
+	p.size = size;
+	dosparts = p :: dosparts;
+	return p;
+}
+
+#
+# Recovery takes care of remembering what the various tables
+# looked like when we started, attempting to restore them when
+# we are finished.
+#
+rtabs: list of ref Recover;
+
+addrecover(t: array of byte, lba: big)
+{
+	tc := array[TableSize+2] of byte;
+	tc[0:] = t[0:len tc];
+	rtabs = ref Recover(tc, lba) :: rtabs;
+}
+
+recover(edit: ref Edit)
+{
+	err := 0;
+	for(rl := rtabs; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(puttable(edit.disk, r.table, r.lba) < 0)
+			err = 1;
+	}
+	if(err) {
+		sys->fprint(stderr, "warning: some writes failed during restoration of old partition tables\n");
+		exits("inconsistent");
+	} else
+		sys->fprint(stderr, "restored old partition tables\n");
+
+	ctlfd := edit.disk.ctlfd;
+	if(ctlfd != nil){
+		offset := edit.disk.offset;
+		for(i:=0; i<len edit.part; i++)
+			if(edit.part[i].ctlname != nil && sys->fprint(ctlfd, "delpart %s", edit.part[i].ctlname)<0)
+				sys->fprint(stderr, "delpart failed: %s: %r", edit.part[i].ctlname);
+		for(i=0; i<len edit.ctlpart; i++)
+			if(edit.part[i].name != nil && sys->fprint(ctlfd, "delpart %s", edit.ctlpart[i].name)<0)
+				sys->fprint(stderr, "delpart failed: %s: %r", edit.ctlpart[i].name);
+		for(i=0; i<len edit.ctlpart; i++){
+			if(sys->fprint(ctlfd, "part %s %bd %bd", edit.ctlpart[i].name,
+				edit.ctlpart[i].start+offset, edit.ctlpart[i].end+offset) < 0){
+				sys->fprint(stderr, "restored disk partition table but not kernel; reboot\n");
+				exits("inconsistent");
+			}
+		}
+	}
+	exits("restored");
+}
+
+#
+# Read the partition table (including extended partition tables)
+# from the disk into the part array.
+#
+rdpart(edit: ref Edit, lba: big, xbase: big)
+{
+	if(xbase == big 0)
+		xbase = lba;	# extended partition in mbr sets the base
+
+	table := gettable(edit.disk, mbroffset+lba, lba == big 0);
+	addrecover(table, mbroffset+lba);
+
+	for(tp := 0; tp<TableSize; tp += TentrySize){
+		dp := PCpart.extract(table[tp:], edit.disk);
+		case dp.ptype {
+		TypeEMPTY =>
+			;
+		TypeEXTENDED or
+		TypeEXTHUGE or
+		TypeLINUXEXT =>
+			rdpart(edit, xbase+dp.offset, xbase);
+		* =>
+			p := mkpart(nil, lba==big 0, lba+dp.offset, dp.size, ref dp);
+			if((err := edit.addpart(p.p)) != nil)
+				sys->fprint(stderr, "error adding partition: %s\n", err);
+		}
+	}
+}
+
+blankpart(edit: ref Edit)
+{
+	edit.changed = 1;
+}
+
+findmbr(edit: ref Edit)
+{
+	table := gettable(edit.disk, big 0, 1);
+	for(tp := 0; tp < TableSize; tp += TentrySize){
+		p := PCpart.extract(table[tp:], edit.disk);
+		if(p.ptype == TypeDMDDO)
+			mbroffset = big edit.disk.s;
+	}
+}
+
+haveroom(edit: ref Edit, primary: int, start: big): int
+{
+	if(primary) {
+		#
+		# must be open primary slot.
+		# primary slots are taken by primary partitions
+		# and runs of secondary partitions.
+		#
+		n := 0;
+		lastsec := 0;
+		for(i:=0; i<len edit.part; i++) {
+			p := tag2part(edit.part[i]);
+			if(p.primary){
+				n++;
+				lastsec = 0;
+			}else if(!lastsec){
+				n++;
+				lastsec = 1;
+			}
+		}
+		return n<4;
+	}
+
+	#
+	# secondary partitions can be inserted between two primary
+	# partitions only if there is an empty primary slot.
+	# otherwise, we can put a new secondary partition next
+	# to a secondary partition no problem.
+	#
+	n := 0;
+	for(i:=0; i<len edit.part; i++){
+		p := tag2part(edit.part[i]);
+		if(p.primary)
+			n++;
+		pend := p.p.end;
+		q: ref Dospart;
+		qstart: big;
+		if(i+1<len edit.part){
+			q = tag2part(edit.part[i+1]);
+			qstart = q.p.start;
+		}else{
+			qstart = edit.end;
+			q = nil;
+		}
+		if(start < pend || start >= qstart)
+			continue;
+		# we go between these two
+		if(p.primary==0 || (q != nil && q.primary==0))
+			return 1;
+	}
+	# not next to a secondary, need a new primary
+	return n<4;
+}
+
+autopart(edit: ref Edit)
+{
+	for(i:=0; i<len edit.part; i++)
+		if(tag2part(edit.part[i]).pc.ptype == Type9)
+			return;
+
+	# look for the biggest gap in which we can put a primary partition
+	start := big 0;
+	bigsize := big 0;
+	bigstart := big 0;
+	for(i=0; i<len edit.part; i++) {
+		p := tag2part(edit.part[i]);
+		if(p.p.start > start && p.p.start - start > bigsize && haveroom(edit, 1, start)) {
+			bigsize = p.p.start - start;
+			bigstart = start;
+		}
+		start = p.p.end;
+	}
+
+	if(edit.end - start > bigsize && haveroom(edit, 1, start)) {
+		bigsize = edit.end - start;
+		bigstart = start;
+	}
+	if(bigsize < big 1) {
+		sys->fprint(stderr, "couldn't find space or partition slot for plan 9 partition\n");
+		return;
+	}
+
+	# set new partition active only if no others are
+	active := Active;	
+	for(i=0; i<len edit.part; i++){
+		p := tag2part(edit.part[i]);
+		if(p.primary && p.pc.active & Active)
+			active = 0;
+	}
+
+	# add new plan 9 partition
+	bigsize *= sec2cyl;
+	bigstart *= sec2cyl;
+	if(bigstart == big 0) {
+		bigstart += big edit.disk.s;
+		bigsize -= big edit.disk.s;
+	}
+	p := mkpart(nil, 1, bigstart, bigsize, nil);
+	p.p.changed = 1;
+	p.pc.active = active;
+	p.pc.ptype = Type9;
+	edit.changed = 1;
+	if((err := edit.addpart(p.p)) != nil){
+		sys->fprint(stderr, "error adding plan9 partition: %s\n", err);
+		return;
+	}
+}
+
+namelist: list of string;
+
+plan9print(part: ref Dospart, fd: ref Sys->FD)
+{
+	vname := types[part.pc.ptype].name;
+	if(vname==nil) {
+		part.p.ctlname = "";
+		return;
+	}
+
+	start := mbroffset+part.lba;
+	end := start+part.size;
+
+	# avoid names like plan90
+	i := len vname - 1;
+	if(isdigit(vname[i]))
+		sep := ".";
+	else
+		sep = "";
+
+	i = 0;
+	name := sys->sprint("%s", vname);
+	ok: int;
+	do {
+		ok = 1;
+		for(nl := namelist; nl != nil; nl = tl nl)
+			if(name == hd nl) {
+				i++;
+				name = sys->sprint("%s%s%d", vname, sep, i);
+				ok = 0;
+			}
+	} while(ok == 0);
+
+	namelist = name :: namelist;
+	part.p.ctlname = name;
+
+	if(fd != nil)
+		sys->print("part %s %bd %bd\n", name, start, end);
+}
+
+cmdprintctl(edit: ref Edit, ctlfd: ref Sys->FD)
+{
+	namelist = nil;
+	for(i:=0; i<len edit.part; i++)
+		plan9print(tag2part(edit.part[i]), nil);
+	edit.ctldiff(ctlfd);
+}
+
+cmdokname(nil: ref Edit, name: string): string
+{
+	if(name[0] != 'p' && name[0] != 's' || len name < 2)
+		return "name must be pN or sN";
+	for(i := 1; i < len name; i++)
+		if(!isdigit(name[i]))
+			return "name must be pN or sN";
+
+	return nil;
+}
+
+KB: con big 1024;
+MB: con KB*KB;
+GB: con KB*MB;
+
+cmdsum(edit: ref Edit, vp: ref Part, a, b: big)
+{
+	if(vp != nil)
+		p := tag2part(vp);
+
+	qual: string;
+	if(p != nil && p.p.changed)
+		qual += "'";
+	else
+		qual += " ";
+	if(p != nil && p.pc.active&Active)
+		qual += "*";
+	else
+		qual += " ";
+
+	if(p != nil)
+		name := p.p.name;
+	else
+		name = "empty";
+	if(p != nil)
+		ty := " "+typestr0(p.pc.ptype);
+	else
+		ty = "";
+
+	sz := (b-a)*big edit.disk.secsize*sec2cyl;
+	suf := "B";
+	div := big 1;
+	if(sz >= big 1*GB){
+		suf = "GB";
+		div = GB;
+	}else if(sz >= big 1*MB){
+		suf = "MB";
+		div = MB;
+	}else if(sz >= big 1*KB){
+		suf = "KB";
+		div = KB;
+	}
+
+	if(div == big 1)
+		sys->print("%s %-12s %*bd %-*bd (%bd cylinders, %bd %s)%s\n", qual, name,
+			edit.disk.width, a, edit.disk.width, b, b-a, sz, suf, ty);
+	else
+		sys->print("%s %-12s %*bd %-*bd (%bd cylinders, %bd.%.2d %s)%s\n", qual, name,
+			edit.disk.width, a, edit.disk.width, b,  b-a,
+			sz/div, int(((sz%div)*big 100)/div), suf, ty);
+}
+
+cmdadd(edit: ref Edit, name: string, start: big, end: big): string
+{
+	if(!haveroom(edit, name[0]=='p', start))
+		return "no room for partition";
+	start *= sec2cyl;
+	end *= sec2cyl;
+	if(start == big 0 || name[0] != 'p')
+		start += big edit.disk.s;
+	p := mkpart(name, name[0]=='p', start, end-start, nil);
+	p.p.changed = 1;
+	p.pc.ptype = Type9;
+	return edit.addpart(p.p);
+}
+
+cmddel(edit: ref Edit, p: ref Part): string
+{
+	return edit.delpart(p);
+}
+
+cmdwrite(edit: ref Edit): string
+{
+	wrpart(edit);
+	return nil;
+}
+
+help: con
+	"A name - set partition active\n"+
+	"P - sys->print table in ctl format\n"+
+	"R - restore disk back to initial configuration and exit\n"+
+	"e - show empty dos partitions\n"+
+	"t name [type] - set partition type\n";
+
+cmdhelp(nil: ref Edit): string
+{
+	sys->print("%s\n", help);
+	return nil;
+}
+
+cmdactive(edit: ref Edit, f: array of string): string
+{
+	if(len f != 2)
+		return "args";
+
+	if(f[1][0] != 'p')
+		return "cannot set secondary partition active";
+
+	if((p := tag2part(edit.findpart(f[1]))) == nil)
+		return "unknown partition";
+
+	for(i:=0; i<len edit.part; i++) {
+		ip := tag2part(edit.part[i]);
+		if(ip.pc.active & Active) {
+			ip.pc.active &= ~Active;
+			ip.p.changed = 1;
+			edit.changed = 1;
+		}
+	}
+
+	if((p.pc.active & Active) == 0) {
+		p.pc.active |= Active;
+		p.p.changed = 1;
+		edit.changed = 1;
+	}
+
+	return nil;
+}
+
+strupr(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] >= 'a' && s[i] <= 'z')
+			s[i] += 'A' - 'a';
+	return s;
+}
+
+dumplist()
+{
+	n := 0;
+	for(i:=0; i<len types; i++) {
+		if(types[i].desc != nil) {
+			sys->print("%-16s", types[i].desc);
+			if(n++%4 == 3)
+				sys->print("\n");
+		}
+	}
+	if(n%4)
+		sys->print("\n");
+}
+
+cmdtype(edit: ref Edit, f: array of string): string
+{
+	if(len f < 2)
+		return "args";
+
+	if((p := tag2part(edit.findpart(f[1]))) == nil)
+		return "unknown partition";
+
+	q: string;
+	if(len f == 2) {
+		for(;;) {
+			sys->fprint(stderr, "new partition type [? for list]: ");
+			q = edit.getline();
+			if(q[0] == '?')
+				dumplist();
+			else
+				break;
+		}
+	} else
+		q = f[2];
+
+	q = strupr(q);
+	for(i:=0; i<len types; i++)
+		if(types[i].desc != nil && types[i].desc == q)
+			break;
+	if(i < len types && p.pc.ptype != i) {
+		p.pc.ptype = i;
+		p.p.changed = 1;
+		edit.changed = 1;
+	}
+	return nil;
+}
+
+cmdext(edit: ref Edit, f: array of string): string
+{
+	case f[0][0] {
+	'A' =>
+		return cmdactive(edit, f);
+	't' =>
+		return cmdtype(edit, f);
+	'R' =>
+		recover(edit);
+		return nil;
+	* =>
+		return "unknown command";
+	}
+}
+
+wrextend(edit: ref Edit, i: int, xbase: big, startlba: big): (int, big)
+{
+	if(i == len edit.part){
+		endlba := edit.disk.secs;
+		if(startlba < endlba)
+			wrzerotab(edit.disk, mbroffset+startlba);
+		return (i, endlba);
+	}
+
+	p := tag2part(edit.part[i]);
+	if(p.primary){
+		endlba := p.p.start*sec2cyl;
+		if(startlba < endlba)
+			wrzerotab(edit.disk, mbroffset+startlba);
+		return (i, endlba);
+	}
+
+	disk := edit.disk;
+	table := gettable(disk, mbroffset+startlba, 0);
+
+	(ni, endlba) := wrextend(edit, i+1, xbase, p.p.end*sec2cyl);
+
+	tp := wrtentry(disk, table[0:], p.pc.active, p.pc.ptype, startlba, startlba+big disk.s, p.p.end*sec2cyl);
+	if(p.p.end*sec2cyl != endlba)
+		tp += wrtentry(disk, table[tp:], 0, TypeEXTENDED, xbase, p.p.end*sec2cyl, endlba);
+
+	for(; tp<TableSize; tp++)
+		table[tp] = byte 0;
+
+	table[Omagic] = byte Magic0;
+	table[Omagic+1] = byte Magic1;
+
+	if(puttable(edit.disk, table, mbroffset+startlba) < 0)
+		recover(edit);
+	return (ni, endlba);
+}
+
+wrzerotab(disk: ref Disk, addr: big)
+{
+	table := array[TableSize+2] of {Omagic => byte Magic0, Omagic+1 => byte Magic1, * => byte 0};
+	if(puttable(disk, table, addr) < 0)
+		recover(edit);
+}
+
+wrpart(edit: ref Edit)
+{	
+	disk := edit.disk;
+
+	table := gettable(disk, mbroffset, 0);
+
+	tp := 0;
+	for(i:=0; i<len edit.part && tp<TableSize; ) {
+		p := tag2part(edit.part[i]);
+		if(p.p.start == big 0)
+			s := big disk.s;
+		else
+			s = p.p.start*sec2cyl;
+		if(p.primary) {
+			tp += wrtentry(disk, table[tp:], p.pc.active, p.pc.ptype, big 0, s, p.p.end*sec2cyl);
+			i++;
+		}else{
+			(ni, endlba) := wrextend(edit, i, p.p.start*sec2cyl, p.p.start*sec2cyl);
+			if(endlba >= big 1024*sec2cyl)
+				t := TypeEXTHUGE;
+			else
+				t = TypeEXTENDED;
+			tp += wrtentry(disk, table[tp:], 0, t, big 0, s, endlba);
+			i = ni;
+		}
+	}
+	for(; tp<TableSize; tp++)
+		table[tp] = byte 0;
+		
+	if(i != len edit.part)
+		raise "wrpart: cannot happen #1";
+
+	if(puttable(disk, table, mbroffset) < 0)
+		recover(edit);
+
+	# bring parts up to date
+	namelist = nil;
+	for(i=0; i<len edit.part; i++)
+		plan9print(tag2part(edit.part[i]), nil);
+
+	if(edit.ctldiff(disk.ctlfd) < 0)
+		sys->fprint(stderr, "?warning: partitions could not be updated in devsd\n");
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+sysfatal(s: string)
+{
+	sys->fprint(stderr, "fdisk: %s\n", s);
+	raise "fail:error";
+}
+
+exits(s: string)
+{
+	if(s != nil)
+		raise "fail:"+s;
+	exit;
+}
+
+assert(i: int)
+{
+	if(!i)
+		raise "assertion failed";
+}
+
+wrtentry(disk: ref Disk, entry: array of byte, active: int, ptype: int, xbase: big, lba: big, end: big): int
+{
+	pc: PCpart;
+	pc.active = active;
+	pc.ptype = ptype;
+	pc.base = xbase;
+	pc.offset = lba-xbase;
+	pc.size = end-lba;
+	entry[0:] = pc.bytes(disk);
+	return TentrySize;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/mkfile	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,26 @@
+<../../../../mkconfig
+
+TARG=\
+	fdisk.dis\
+	pedit.dis\
+	prep.dis\
+	calc.tab.dis\
+
+MODULES=\
+	pedit.m\
+
+SYSMODULES=\
+	arg.m\
+	sys.m\
+	draw.m\
+	disks.m\
+	bufio.m\
+	string.m\
+
+DISBIN=$ROOT/dis/disk
+
+<$ROOT/mkfiles/mkdis
+
+# calc
+calc.tab.b:
+	yacc -s calc -d calc.y
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/pedit.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,503 @@
+implement Pedit;
+
+#
+# disk partition editor
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "disks.m";
+	disks: Disks;
+	Disk: import disks;
+
+include "draw.m";
+include "calc.tab.m";
+	calc: Calc;
+
+include "pedit.m";
+
+Cmd: adt {
+	c: int;
+	f:	ref fn(e: ref Edit, a: array of string): string;
+};
+
+cmds: array of Cmd;
+
+bin: ref Iobuf;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	calc = load Calc "/dis/disk/calc.tab.dis";
+	bufio = load Bufio Bufio->PATH;
+	disks = load Disks Disks->PATH;
+	disks->init();
+
+	bin = bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	cmds = array[] of {
+		('.',	editdot),
+		('a',	editadd),
+		('d',	editdel),
+		('?',	edithelp),
+		('h',	edithelp),
+		('P',	editctlprint),
+		('p',	editprint),
+		('w',	editwrite),
+		('q',	editquit),
+	};
+}
+
+Edit.mk(unit: string): ref Edit
+{
+	e := ref Edit;
+	e.unit = unit;
+	e.dot = big 0;
+	e.end = big 0;
+	e.changed = 0;
+	e.warned = 0;
+	e.lastcmd = 0;
+	return e;
+}
+
+Edit.getline(edit: self ref Edit): string
+{
+	p := bin.gets('\n');
+	if(p == nil){
+		if(edit.changed)
+			sys->fprint(sys->fildes(2), "?warning: changes not written\n");
+		exit;
+	}
+	for(i := 0; i < len p; i++)
+		if(!isspace(p[i]))
+			break;
+	if(i)
+		return p[i:];
+	return p;
+}
+
+Edit.findpart(edit: self ref Edit, name: string): ref Part
+{
+	for(i:=0; i<len edit.part; i++)
+		if(edit.part[i].name == name)
+			return edit.part[i];
+	return nil;
+}
+
+okname(edit: ref Edit, name: string): string
+{
+	if(name[0] == '\0')
+		return "partition has no name";
+
+	for(i:=0; i<len edit.part; i++) {
+		if(name == edit.part[i].name)
+			return sys->sprint("already have partition with name '%s'", name);
+	}
+	return nil;
+}
+
+Edit.addpart(edit: self ref Edit, p: ref Part): string
+{
+	if((err := okname(edit, p.name)) != nil)
+		return err;
+
+	for(i:=0; i<len edit.part; i++) {
+		if(p.start < edit.part[i].end && edit.part[i].start < p.end) {
+			msg := sys->sprint("\"%s\" %bd-%bd overlaps with \"%s\" %bd-%bd",
+				p.name, p.start, p.end,
+				edit.part[i].name, edit.part[i].start, edit.part[i].end);
+		#	return msg;
+		}
+	}
+
+	if(len edit.part >= Maxpart)
+		return "too many partitions";
+
+	pa := array[i+1] of ref Part;
+	pa[0:] = edit.part;
+	edit.part = pa;
+
+	edit.part[i] = p;
+	for(; i > 0 && p.start < edit.part[i-1].start; i--) {
+		edit.part[i] = edit.part[i-1];
+		edit.part[i-1] = p;
+	}
+
+	if(p.changed)
+		edit.changed = 1;
+	return nil;
+}
+
+Edit.delpart(edit: self ref Edit, p: ref Part): string
+{
+	n := len edit.part;
+	for(i:=0; i<n; i++)
+		if(edit.part[i] == p)
+			break;
+	if(i >= n)
+		raise "internal error: Part not found";
+	n--;
+	pa := array[n] of ref Part;
+	if(n){
+		pa[0:] = edit.part[0:i];
+		if(i != n)
+			pa[i:] = edit.part[i+1:];
+	}
+	edit.part = pa;
+	edit.changed = 1;
+	return nil;
+}
+
+editdot(edit: ref Edit, argv: array of string): string
+{
+	if(len argv == 1) {
+		sys->print("\t. %bd\n", edit.dot);
+		return nil;
+	}
+
+	if(len argv > 2)
+		return "args";
+
+	(ndot, err) := calc->parseexpr(argv[1], edit.dot, edit.end, edit.end);
+	if(err != nil)
+		return err;
+
+	edit.dot = ndot;
+	return nil;
+}
+
+editadd(edit: ref Edit, argv: array of string): string
+{
+	if(len argv < 2)
+		return "args";
+
+	name := argv[1];
+	if((err := okname(edit, name)) != nil || edit.okname != nil && (err = edit.okname(edit, name)) != nil)
+		return err;
+
+	if(len argv >= 3)
+		q := argv[2];
+	else {
+		sys->fprint(sys->fildes(2), "start %s: ", edit.unit);
+		q = edit.getline();
+	}
+	start: big;
+	(start, err) = calc->parseexpr(q, edit.dot, edit.end, edit.end);
+	if(err != nil)
+		return err;
+
+	if(start < big 0 || start >= edit.end)
+		return "start out of range";
+
+	for(i:=0; i < len edit.part; i++) {
+		if(edit.part[i].start <= start && start < edit.part[i].end)
+			return sys->sprint("start %s in partition '%s'", edit.unit, edit.part[i].name);
+	}
+
+	maxend := edit.end;
+	for(i=0; i < len edit.part; i++)
+		if(start < edit.part[i].start && edit.part[i].start < maxend)
+			maxend = edit.part[i].start;
+
+	if(len argv >= 4)
+		q = argv[3];
+	else {
+		sys->fprint(sys->fildes(2), "end [%bd..%bd] ", start, maxend);
+		q = edit.getline();
+	}
+	end: big;
+	(end, err) = calc->parseexpr(q, edit.dot, maxend, edit.end);
+	if(err != nil)
+		return err;
+
+	if(start == end)
+		return "size zero partition";
+
+	if(end <= start || end > maxend)
+		return "end out of range";
+
+	if(len argv > 4)
+		return "args";
+
+	if((err = edit.add(edit, name, start, end)) != nil)
+		return err;
+
+	edit.dot = end;
+	return nil;
+}
+
+editdel(edit: ref Edit, argv: array of string): string
+{
+	if(len argv != 2)
+		return "args";
+
+	if((p := edit.findpart(argv[1])) == nil)
+		return "no such partition";
+
+	return edit.del(edit, p);
+}
+
+helptext :=
+	". [newdot] - display or set value of dot\n"+
+	"a name [start [end]] - add partition\n"+
+	"d name - delete partition\n"+
+	"h - sys->print help message\n"+
+	"p - sys->print partition table\n"+
+	"P - sys->print commands to update sd(3) device\n"+
+	"w - write partition table\n"+
+	"q - quit\n";
+
+edithelp(edit: ref Edit, nil: array of string): string
+{
+	sys->print("%s", helptext);
+	if(edit.help != nil)
+		return edit.help(edit);
+	return nil;
+}
+
+editprint(edit: ref Edit, argv: array of string): string
+{
+	if(len argv != 1)
+		return "args";
+
+	lastend := big 0;
+	part := edit.part;
+	for(i:=0; i<len edit.part; i++) {
+		if(lastend < part[i].start)
+			edit.sum(edit, nil, lastend, part[i].start);
+		edit.sum(edit, part[i], part[i].start, part[i].end);
+		lastend = part[i].end;
+	}
+	if(lastend < edit.end)
+		edit.sum(edit, nil, lastend, edit.end);
+	return nil;
+}
+
+editwrite(edit: ref Edit, argv: array of string): string
+{
+	if(len argv != 1)
+		return "args";
+
+	if(edit.disk.rdonly)
+		return "read only";
+
+	err := edit.write(edit);
+	if(err != nil)
+		return err;
+	for(i:=0; i<len edit.part; i++)
+		edit.part[i].changed = 0;
+	edit.changed = 0;
+	return nil;
+}
+
+editquit(edit: ref Edit, argv: array of string): string
+{
+	if(len argv != 1) {
+		edit.warned = 0;
+		return "args";
+	}
+
+	if(edit.changed && (!edit.warned || edit.lastcmd != 'q')) {
+		edit.warned = 1;
+		return "changes unwritten";
+	}
+
+	exit;
+}
+
+editctlprint(edit: ref Edit, argv: array of string): string
+{
+	if(len argv != 1)
+		return "args";
+
+	if(edit.printctl != nil)
+		edit.printctl(edit, sys->fildes(1));
+	else
+		edit.ctldiff(sys->fildes(1));
+	return nil;
+}
+
+Edit.runcmd(edit: self ref Edit, cmd: string)
+{
+	(nf, fl) := sys->tokenize(cmd, " \t\n\r");
+	if(nf < 1)
+		return;
+	f := array[nf] of string;
+	for(nf = 0; fl != nil; fl = tl fl)
+		f[nf++] = hd fl;
+	if(len f[0] != 1) {
+		sys->fprint(sys->fildes(2), "?\n");
+		return;
+	}
+
+	err := "";
+	for(i:=0; i<len cmds; i++) {
+		if(cmds[i].c == f[0][0]) {
+			op := cmds[i].f;
+			err = op(edit, f);
+			break;
+		}
+	}
+	if(i == len cmds){
+		if(edit.ext != nil)
+			err = edit.ext(edit, f);
+		else
+			err = "unknown command";
+	}
+	if(err != nil) 
+		sys->fprint(sys->fildes(2), "?%s\n", err);
+	edit.lastcmd = f[0][0];
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+ctlmkpart(name: string, start: big, end: big, changed: int): ref Part
+{
+	p := ref Part;
+	p.name = name;
+	p.ctlname = name;
+	p.start = start;
+	p.end = end;
+	p.ctlstart = big 0;
+	p.ctlend = big 0;
+	p.changed = changed;
+	return p;
+}
+
+rdctlpart(edit: ref Edit)
+{
+	disk := edit.disk;
+	edit.ctlpart = array[0] of ref Part;
+	sys->seek(disk.ctlfd, big 0, 0);
+	buf := array[4096] of byte;
+	if(sys->readn(disk.ctlfd, buf, len buf) <= 0)
+		return;
+	for(i := 0; i < len buf; i++)
+		if(buf[i] == byte 0)
+			break;
+
+	(nline, lines) := sys->tokenize(string buf[0:i], "\n\r");
+	edit.ctlpart = array[nline] of ref Part;	# upper bound
+	npart := 0;
+	for(i=0; i<nline; i++){
+		line := hd lines;
+		lines = tl lines;
+		if(len line < 5 || line[0:5] != "part ")
+			continue;
+
+		(nf, f) := sys->tokenize(line, " \t");
+		if(nf != 4 || hd f != "part")
+			break;
+
+		a := big hd tl tl f;
+		b := big hd tl tl tl f;
+
+		if(a >= b)
+			break;
+
+		# only gather partitions contained in the disk partition we are editing
+		if(a < disk.offset ||  disk.offset+disk.secs < b)
+			continue;
+
+		a -= disk.offset;
+		b -= disk.offset;
+
+		# the partition we are editing does not count
+		if(hd tl f == disk.part)
+			continue;
+
+		edit.ctlpart[npart++] = ctlmkpart(hd tl f, a, b, 0);
+	}
+	if(npart != len edit.ctlpart)
+		edit.ctlpart = edit.ctlpart[0:npart];
+}
+
+ctlstart(p: ref Part): big
+{
+	if(p.ctlstart != big 0)
+		return p.ctlstart;
+	return p.start;
+}
+
+ctlend(p: ref Part): big
+{
+	if(p.ctlend != big 0)
+		return p.ctlend;
+	return p.end;
+}
+
+areequiv(p: ref Part, q: ref Part): int
+{
+	if(p.ctlname == nil || q.ctlname == nil)
+		return 0;
+	return p.ctlname == q.ctlname &&
+			ctlstart(p) == ctlstart(q) && ctlend(p) == ctlend(q);
+}
+
+unchange(edit: ref Edit, p: ref Part)
+{
+	for(i:=0; i<len edit.ctlpart; i++) {
+		q := edit.ctlpart[i];
+		if(p.start <= q.start && q.end <= p.end)
+			q.changed = 0;
+	}
+	if(p.changed)
+		raise "internal error: Part unchanged";
+}
+
+Edit.ctldiff(edit: self ref Edit, ctlfd: ref Sys->FD): int
+{
+	rdctlpart(edit);
+
+	# everything is bogus until we prove otherwise
+	for(i:=0; i<len edit.ctlpart; i++)
+		edit.ctlpart[i].changed = 1;
+
+	#
+	# partitions with same info have not changed,
+	# and neither have partitions inside them.
+	#
+	for(i=0; i<len edit.ctlpart; i++)
+		for(j:=0; j<len edit.part; j++)
+			if(areequiv(edit.ctlpart[i], edit.part[j])) {
+				unchange(edit, edit.ctlpart[i]);
+				break;
+			}
+
+	waserr := 0;
+	#
+	# delete all the changed partitions except data (we'll add them back if necessary) 
+	#
+	for(i=0; i<len edit.ctlpart; i++) {
+		p := edit.ctlpart[i];
+		if(p.changed)
+		if(sys->fprint(ctlfd, "delpart %s\n", p.ctlname)<0) {
+			sys->fprint(sys->fildes(2), "delpart failed: %s: %r\n", p.ctlname);
+			waserr = -1;
+		}
+	}
+
+	#
+	# add all the partitions from the real list;
+	# this is okay since adding a partition with
+	# information identical to what is there is a no-op.
+	#
+	offset := edit.disk.offset;
+	for(i=0; i<len edit.part; i++) {
+		p := edit.part[i];
+		if(p.ctlname != nil) {
+			if(sys->fprint(ctlfd, "part %s %bd %bd\n", p.ctlname, offset+ctlstart(p), offset+ctlend(p)) < 0) {
+				sys->fprint(sys->fildes(2), "adding part failed: %s: %r\n", p.ctlname);
+				waserr = -1;
+			}
+		}
+	}
+	return waserr;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/pedit.m	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,53 @@
+Pedit: module
+{
+	PATH: con "/dis/disk/pedit.dis";
+
+	Part: adt {
+		name:	string;
+		ctlname:	string;
+		start:		big;
+		end:		big;
+		ctlstart:	big;
+		ctlend:	big;
+		changed:	int;
+		tag:		int;
+	};
+
+	Maxpart: con 32;
+
+	Edit: adt {
+		disk:	ref Disks->Disk;
+
+		ctlpart:	array of ref Part;
+		part:	array of ref Part;
+
+		# to do: replace by channels
+		add:	ref fn(e: ref Edit, s: string, a, b: big): string;
+		del:	ref fn(e: ref Edit, p: ref Part): string;
+		ext:	ref fn(e: ref Edit, f: array of string): string;
+		help:	ref fn(e: ref Edit): string;
+		okname:	ref fn(e: ref Edit, s: string): string;
+		sum:	ref fn(e: ref Edit, p: ref Part, a, b: big);
+		write:	ref fn(e: ref Edit): string;
+		printctl:	ref fn(e: ref Edit, x: ref Sys->FD);
+
+		unit:	string;
+		dot:	big;
+		end:	big;
+
+		# do not use fields below this line
+		changed:	int;
+		warned:	int;
+		lastcmd:	int;
+
+		mk:	fn(unit: string): ref Edit;
+		getline:	fn(e: self ref Edit): string;
+		runcmd:	fn(e: self ref Edit, c: string);
+		findpart:	fn(e: self ref Edit, n: string): ref Part;
+		addpart:	fn(e: self ref Edit, p: ref Part): string;
+		delpart:	fn(e: self ref Edit, p: ref Part): string;
+		ctldiff:	fn(e: self ref Edit, ctlfd: ref Sys->FD): int;
+	};
+
+	init:	fn();
+};
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/disk/prep/prep.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,508 @@
+implement Prep;
+
+#
+# prepare plan 9/inferno disk partition
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "disks.m";
+	disks: Disks;
+	Disk: import disks;
+
+include "pedit.m";
+	pedit: Pedit;
+	Edit, Part: import pedit;
+
+include "arg.m";
+
+Prep: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+blank := 0;
+file := 0;
+doauto := 0;
+printflag := 0;
+opart: array of ref Part;
+secbuf: array of byte;
+osecbuf: array of byte;
+zeroes: array of byte;
+rdonly := 0;
+dowrite := 0;
+
+Prepedit: type Edit[string];
+
+edit: ref Edit;
+
+Auto: adt
+{
+	name:	string;
+	min:		big;
+	max:		big;
+	weight:	int;
+	alloc:	int;
+	size:		big;
+};
+
+KB: con big 1024;
+MB: con KB*KB;
+GB: con KB*MB;
+
+#
+# Order matters -- this is the layout order on disk.
+#
+auto: array of Auto = array[] of {
+	("9fat",		big 10*MB,	big 100*MB,	10, 0, big 0),
+	("nvram",	big 512,	big 512,	1, 0, big 0),
+	("fscfg",	big 512,	big 512,	1, 0, big 0),
+	("fs",		big 200*MB,	big 0,	10, 0, big 0),
+	("fossil",	big 200*MB,	big 0,	4, 0, big 0),
+	("arenas",	big 500*MB,	big 0,	20, 0, big 0),
+	("isect",	big 25*MB,	big 0,	1, 0, big 0),
+	("other",	big 200*MB,	big 0,	4, 0, big 0),
+	("swap",		big 100*MB,	big 512*MB,	1, 0, big 0),
+	("cache",	big 50*MB,	big 1*GB,	2, 0, big 0),
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	disks = load Disks Disks->PATH;
+	pedit = load Pedit Pedit->PATH;
+
+	sys->pctl(Sys->FORKFD, nil);
+	disks->init();
+	pedit->init();
+
+	edit = Edit.mk("sector");
+
+	edit.add = cmdadd;
+	edit.del = cmddel;
+	edit.okname = cmdokname;
+	edit.sum = cmdsum;
+	edit.write = cmdwrite;
+
+	stderr = sys->fildes(2);
+	secsize := 0;
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("disk/prep [-bfprw] [-a partname]... [-s sectorsize] /dev/sdC0/plan9");
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>
+			p := arg->earg();
+			for(i:=0; i<len auto; i++){
+				if(p == auto[i].name){
+					if(auto[i].alloc){
+						sys->fprint(stderr, "you said -a %s more than once.\n", p);
+						arg->usage();
+					}
+					auto[i].alloc = 1;
+					break;
+				}
+			}
+			if(i == len auto){
+				sys->fprint(stderr, "don't know how to create automatic partition %s\n", p);
+				arg->usage();
+			}
+			doauto = 1;
+		'b' =>
+			blank++;
+		'f' =>
+			file++;
+		'p' =>
+			printflag++;
+			rdonly++;
+		'r' =>
+			rdonly++;
+		's' =>
+			secsize = int arg->earg();
+		'w' =>
+			dowrite++;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	arg = nil;
+
+	mode := Sys->ORDWR;
+	if(rdonly)
+		mode = Sys->OREAD;
+	disk := Disk.open(hd args, mode, file);
+	if(disk == nil) {
+		sys->fprint(stderr, "cannot open disk: %r\n");
+		exits("opendisk");
+	}
+
+	if(secsize != 0) {
+		disk.secsize = secsize;
+		disk.secs = disk.size / big secsize;
+	}
+	edit.end = disk.secs;
+
+	checkfat(disk);
+
+	secbuf = array[disk.secsize+1] of byte;
+	osecbuf = array[disk.secsize+1] of byte;
+	zeroes = array[disk.secsize+1] of {* => byte 0};
+	edit.disk = disk;
+
+	if(blank == 0)
+		rdpart(edit);
+
+	# save old partition table
+	opart = array[len edit.part] of ref Part;
+	opart[0:] = edit.part;
+
+	if(printflag) {
+		edit.runcmd("P");
+		exits(nil);
+	}
+
+	if(doauto)
+		autopart(edit);
+
+	if(dowrite) {
+		edit.runcmd("w");
+		exits(nil);
+	}
+
+	edit.runcmd("p");
+	for(;;) {
+		sys->fprint(stderr, ">>> ");
+		edit.runcmd(edit.getline());
+	}
+}
+
+cmdsum(edit: ref Edit, p: ref Part, a: big, b: big)
+{
+	c := ' ';
+	name := "empty";
+	if(p != nil){
+		if(p.changed)
+			c = '\'';
+		name = p.name;
+	}
+
+	sz := (b-a)*big edit.disk.secsize;
+	suf := "B ";
+	div := big 1;
+	if(sz >= big 1*GB){
+		suf = "GB";
+		div = GB;
+	}else if(sz >= big 1*MB){
+		suf = "MB";
+		div = MB;
+	}else if(sz >= big 1*KB){
+		suf = "KB";
+		div = KB;
+	}
+
+	if(div == big 1)
+		sys->print("%c %-12s %*bd %-*bd (%bd sectors, %bd %s)\n", c, name,
+			edit.disk.width, a, edit.disk.width, b, b-a, sz, suf);
+	else
+		sys->print("%c %-12s %*bd %-*bd (%bd sectors, %bd.%.2d %s)\n", c, name,
+			edit.disk.width, a, edit.disk.width, b, b-a,
+			sz/div, int (((sz%div)*big 100)/div), suf);
+}
+
+cmdadd(edit: ref Edit, name: string, start: big, end: big): string
+{
+	if(start < big 2 && name == "9fat")
+		return "overlaps with the pbs and/or the partition table";
+
+	return edit.addpart(mkpart(name, start, end, 1));
+}
+
+cmddel(edit: ref Edit, p: ref Part): string
+{
+	return edit.delpart(p);
+}
+
+cmdwrite(edit: ref Edit): string
+{
+	wrpart(edit);
+	return nil;
+}
+
+isfrog := array[256] of {
+	byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1,	# NUL
+	byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1,	# BKS
+	byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1,	# DLE
+	byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1, byte 1,	# CAN
+	' ' =>	byte 1,
+	'/' =>	byte 1,
+	16r7f=>	byte 1,
+	* => byte 0
+};
+
+cmdokname(nil: ref Edit, elem: string): string
+{
+	for(i := 0; i < len elem; i++)
+		if(int isfrog[elem[i]])
+			return "bad character in name";
+	return nil;
+}
+
+mkpart(name: string, start: big, end: big, changed: int): ref Part
+{
+	p := ref Part;
+	p.name = name;
+	p.ctlname = name;
+	p.start = start;
+	p.end = end;
+	p.changed = changed;
+	p.ctlstart = big 0;
+	p.ctlend = big 0;
+	return p;
+}
+
+# plan9 partition table is first sector of the disk
+
+rdpart(edit: ref Edit)
+{
+	disk := edit.disk;
+	sys->seek(disk.fd, big disk.secsize, 0);
+	if(sys->readn(disk.fd, osecbuf, disk.secsize) != disk.secsize)
+		return;
+	osecbuf[disk.secsize] = byte 0;
+	secbuf[0:] = osecbuf;
+
+	for(i := 0; i < disk.secsize; i++)
+		if(secbuf[i] == byte 0)
+			break;
+
+	tab := string secbuf[0:i];
+	if(len tab < 4 || tab[0:4] != "part"){
+		sys->fprint(stderr, "no plan9 partition table found\n");
+		return;
+	}
+
+	waserr := 0;
+	(nline, lines) := sys->tokenize(tab, "\n");
+	for(i=0; i<nline; i++){
+		line := hd lines;
+		lines = tl lines;
+		if(len line < 4 || line[0:4] != "part"){
+			waserr = 1;
+			continue;
+		}
+
+		(nf, f) := sys->tokenize(line, " \t\r");
+		if(nf != 4 || hd f != "part"){
+			waserr = 1;
+			continue;
+		}
+
+		a := big hd tl tl f;
+		b := big hd tl tl tl f;
+		if(a >= b){
+			waserr = 1;
+			continue;
+		}
+
+		if((err := edit.addpart(mkpart(hd tl f, a, b, 0))) != nil) {
+			sys->fprint(stderr, "?%s: not continuing\n", err);
+			exits("partition");
+		}
+	}
+	if(waserr)
+		sys->fprint(stderr, "syntax error reading partition\n");
+}
+
+min(a, b: big): big
+{
+	if(a < b)
+		return a;
+	return b;
+}
+
+autopart(edit: ref Edit)
+{
+	if(len edit.part > 0) {
+		if(doauto)
+			sys->fprint(stderr, "partitions already exist; not repartitioning\n");
+		return;
+	}
+
+	secs := edit.disk.secs;
+	secsize := big edit.disk.secsize;
+	for(;;){
+		# compute total weights
+		totw := 0;
+		for(i:=0; i<len auto; i++){
+			if(auto[i].alloc==0 || auto[i].size != big 0)
+				continue;
+			totw += auto[i].weight;
+		}
+		if(totw == 0)
+			break;
+
+		if(secs <= big 0){
+			sys->fprint(stderr, "ran out of disk space during autopartition.\n");
+			return;
+		}
+
+		# assign any minimums for small disks
+		futz := 0;
+		for(i=0; i<len auto; i++){
+			if(auto[i].alloc==0 || auto[i].size != big 0)
+				continue;
+			s := (secs*big auto[i].weight)/big totw;
+			if(s < big auto[i].min/secsize){
+				auto[i].size = big auto[i].min/secsize;
+				secs -= auto[i].size;
+				futz = 1;
+				break;
+			}
+		}
+		if(futz)
+			continue;
+
+		# assign any maximums for big disks
+		futz = 0;
+		for(i=0; i<len auto; i++){
+			if(auto[i].alloc==0 || auto[i].size != big 0)
+				continue;
+			s := (secs*big auto[i].weight)/big totw;
+			if(auto[i].max != big 0 && s > auto[i].max/secsize){
+				auto[i].size = auto[i].max/secsize;
+				secs -= auto[i].size;
+				futz = 1;
+				break;
+			}
+		}
+		if(futz)
+			continue;
+
+		# finally, assign partition sizes according to weights
+		for(i=0; i<len auto; i++){
+			if(auto[i].alloc==0 || auto[i].size != big 0)
+				continue;
+			s := (secs*big auto[i].weight)/big totw;
+			auto[i].size = s;
+
+			# use entire disk even in face of rounding errors
+			secs -= auto[i].size;
+			totw -= auto[i].weight;
+		}
+	}
+
+	for(i:=0; i<len auto; i++)
+		if(auto[i].alloc)
+			sys->print("%s %bud\n", auto[i].name, auto[i].size);
+
+	s := big 0;
+	for(i=0; i<len auto; i++){
+		if(auto[i].alloc == 0)
+			continue;
+		if((err := edit.addpart(mkpart(auto[i].name, s, s+auto[i].size, 1))) != nil)
+			sys->fprint(stderr, "addpart %s: %s\n", auto[i].name, err);
+		s += auto[i].size;
+	}
+}
+
+restore(edit: ref Edit, ctlfd: ref Sys->FD)
+{
+	offset := edit.disk.offset;
+	sys->fprint(stderr, "attempting to restore partitions to previous state\n");
+	if(sys->seek(edit.disk.wfd, big edit.disk.secsize, 0) != big 0){
+		sys->fprint(stderr, "cannot restore: error seeking on disk: %r\n");
+		exits("inconsistent");
+	}
+
+	if(sys->write(edit.disk.wfd, osecbuf, edit.disk.secsize) != edit.disk.secsize){
+		sys->fprint(stderr, "cannot restore: couldn't write old partition table to disk: %r\n");
+		exits("inconsistent");
+	}
+
+	if(ctlfd != nil){
+		for(i:=0; i<len edit.part; i++)
+			sys->fprint(ctlfd, "delpart %s", edit.part[i].name);
+		for(i=0; i<len opart; i++){
+			if(sys->fprint(ctlfd, "part %s %bd %bd", opart[i].name, opart[i].start+offset, opart[i].end+offset) < 0){
+				sys->fprint(stderr, "restored disk partition table but not kernel table; reboot\n");
+				exits("inconsistent");
+			}
+		}
+	}
+	exits("restored");
+}
+
+wrpart(edit: ref Edit)
+{
+	disk := edit.disk;
+
+	secbuf[0:] = zeroes;
+	n := 0;
+	for(i:=0; i<len edit.part; i++){
+		a := sys->aprint("part %s %bd %bd\n", 
+			edit.part[i].name, edit.part[i].start, edit.part[i].end);
+		if(n + len a > disk.secsize){
+			sys->fprint(stderr, "partition table bigger than sector (%d bytes)\n", disk.secsize);
+			exits("overflow");
+		}
+		secbuf[n:] = a;
+		n += len a;
+	}
+
+	if(sys->seek(disk.wfd, big disk.secsize, 0) != big disk.secsize){
+		sys->fprint(stderr, "error seeking to %d on disk: %r\n", disk.secsize);
+		exits("seek");
+	}
+
+	if(sys->write(disk.wfd, secbuf, disk.secsize) != disk.secsize){
+		sys->fprint(stderr, "error writing partition table to disk: %r\n");
+		restore(edit, nil);
+	}
+
+	if(edit.ctldiff(disk.ctlfd) < 0)
+		sys->fprint(stderr, "?warning: partitions could not be updated in devsd\n");
+}
+
+#
+# Look for a boot sector in sector 1, as would be
+# the case if editing /dev/sdC0/data when that
+# was really a bootable disk.
+#
+checkfat(disk: ref Disk)
+{
+	buf := array[32] of byte;
+
+	if(sys->seek(disk.fd, big disk.secsize, 0) != big disk.secsize ||
+	   sys->read(disk.fd, buf, len buf) < len buf)
+		return;
+
+	if(buf[0] != byte 16rEB || buf[1] != byte 16r3C || buf[2] != byte 16r90)
+		return;
+
+	sys->fprint(stderr, 
+		"there's a fat partition where the\n"+
+		"plan9 partition table would go.\n"+
+		"if you really want to overwrite it, zero\n"+
+		"the second sector of the disk and try again\n");
+
+	exits("fat partition");
+}
+
+exits(s: string)
+{
+	if(s != nil)
+		raise "fail:"+s;
+	exit;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/examples/minitel/mdisplay.b	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,799 @@
+implement MDisplay;
+
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+# - best viewed with acme!
+
+include "sys.m";
+include "draw.m";
+include "mdisplay.m";
+
+sys		: Sys;
+draw		: Draw;
+
+Context, Point, Rect, Font, Image, Display, Screen : import draw;
+
+
+# len cell		== number of lines
+# len cell[0]	== number of cellmap cells per char
+# (x,y)*cellsize	== font glyph clipr
+
+cellS		:= array [] of {array [] of {(0, 0)}};
+cellW	:= array [] of {array [] of {(0, 0), (1, 0)}};
+cellH		:= array [] of {array [] of {(0, 1)}, array [] of {(0, 0)}};
+cellWH	:= array [] of {array [] of {(0, 1), (1, 1)}, array [] of {(0, 0), (1, 0)}};
+
+Cellinfo : adt {
+	font		: ref Font;
+	ch, attr	: int;
+	clipmod	: (int, int);
+};
+
+
+# current display attributes
+display	: ref Display;
+window	: ref Image;
+frames	:= array [2] of ref Image;
+update	: chan of int;
+
+colours	: array of ref Image;
+bright	: ref Image;
+
+# current mode attributes
+cellmap	: array of Cellinfo;
+nrows	: int;
+ncols	: int;
+ulheight	: int;
+curpos	: Point;
+winoff	: Point;
+cellsize	: Point;
+modeattr	: con fgWhite | bgBlack;
+showC	:= 0;
+delims	:= 0;
+modbbox := Rect((0,0),(0,0));
+blankrow	: array of Cellinfo;
+
+ctxt		: ref Context;
+font		: ref Font;	# g0 videotex font - extended with unicode g2 syms
+fonth	: ref Font;	# double height version of font
+fontw	: ref Font;	# double width
+fonts		: ref Font;	# double size
+fontg1	: ref Font;	# semigraphic videotex font (ch+128=separated)
+fontfr	: ref Font;	# french character set
+fontusa	: ref Font;	# american character set
+
+
+Init(c : ref Context) : string
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+
+	if (c == nil || c.display == nil)
+		return "no display context";
+
+	ctxt = c;
+	disp := ctxt.display;
+
+	black	:= disp.rgb2cmap(0, 0, 0);
+	blue		:= disp.rgb2cmap(0, 0, 255);
+	red		:= disp.rgb2cmap(255, 0, 0);
+	magenta	:= disp.rgb2cmap(255, 0, 255);
+	green	:= disp.rgb2cmap(0, 255, 0);
+	cyan		:= disp.rgb2cmap(0, 255, 255);
+	yellow	:= disp.rgb2cmap(255, 255, 0);
+	white	:= disp.rgb2cmap(240, 240, 240);
+
+	iblack	:= disp.color(black);
+	iblue		:= disp.color(blue);
+	ired		:= disp.color(red);
+	imagenta	:= disp.color(magenta);
+	igreen	:= disp.color(green);
+	icyan	:= disp.color(cyan);
+	iyellow	:= disp.color(yellow);
+	iwhite	:= disp.color(white);
+
+	colours	= array [] of {	iblack, iblue, ired, imagenta,
+						igreen, icyan, iyellow, iwhite};
+	bright	= disp.color(disp.rgb2cmap(255, 255, 255));
+	
+	update = chan of int;
+	spawn Update(update);
+	display = disp;
+	return nil;
+}
+
+Quit()
+{
+	if (update != nil)
+		update <- = QuitUpdate;
+	update	= nil;
+	window	= nil;
+	frames[0]	= nil;
+	frames[1]	= nil;
+	cellmap	= nil;
+	display	= nil;
+}
+
+Mode(r : Draw->Rect, w, h, ulh, d : int, fontpath : string) : (string, ref Draw->Image)
+{
+	if (display == nil)
+		# module not properly Init()'d
+		return ("not initialized", nil);
+
+	curpos = Point(-1, -1);
+	if (window != nil)
+		update <- = Pause;
+
+	cellmap = nil;
+	window = nil;
+	(dx, dy) := (r.dx(), r.dy());
+	if (dx == 0 || dy == 0) {
+		return (nil, nil);
+	}
+
+	black := display.rgb2cmap(0, 0, 0);
+	window = ctxt.screen.newwindow(r, Draw->Refbackup, black);
+	if (window == nil)
+		return ("cannot create window", nil);
+
+	window.origin(Point(0,0), r.min);
+	winr := Rect((0,0), (dx, dy));
+	frames[0] = display.newimage(winr, window.chans, 0, black);
+	frames[1] = display.newimage(winr, window.chans, 0, black);
+
+	if (window == nil || frames[0] == nil || frames[1] == nil) {
+		window = nil;
+		return ("cannot allocate display resources", nil);
+	}
+
+	ncols = w;
+	nrows = h;
+	ulheight = ulh;
+	delims = d;
+	showC = 0;
+
+	cellmap = array [ncols * nrows] of Cellinfo;
+	
+	font		= Font.open(display, fontpath);
+	fontw	= Font.open(display, fontpath + "w");
+	fonth	= Font.open(display, fontpath + "h");
+	fonts		= Font.open(display, fontpath + "s");
+	fontg1	= Font.open(display, fontpath + "g1");
+	fontfr	= Font.open(display, fontpath + "fr");
+	fontusa	= Font.open(display, fontpath + "usa");
+
+	if (font != nil)
+		cellsize = Point(font.width(" "), font.height);
+	else
+		cellsize = Point(dx/ncols, dy / nrows);
+
+	winoff.x = (dx - (cellsize.x * ncols)) / 2;
+	winoff.y = (dy - (cellsize.y * nrows)) /2;
+	if (winoff.x < 0)
+		winoff.x = 0;
+	if (winoff.y < 0)
+		winoff.y = 0;
+
+	blankrow = array [ncols] of {* => Cellinfo(font, ' ', modeattr | fgWhite, (0,0))};
+	for (y := 0; y < nrows; y++) {
+		col0 := y * ncols;
+		cellmap[col0:] = blankrow;
+	}
+
+#	frames[0].clipr = frames[0].r;
+#	frames[1].clipr = frames[1].r;
+#	frames[0].draw(frames[0].r, colours[0], nil, Point(0,0));
+#	frames[1].draw(frames[1].r, colours[0], nil, Point(0,0));
+#	window.draw(window.r, colours[0], nil, Point(0,0));
+	update <- = Continue;
+	return (nil, window);
+}
+
+Cursor(pt : Point)
+{
+	if (update == nil || cellmap == nil)
+		# update thread (cursor/character flashing) not running
+		return;
+
+	# normalize pt
+	pt.x--;
+
+	curpos = pt;
+	update <- = CursorSet;
+}
+
+Put(str : string, pt : Point, charset, attr, insert : int)
+{
+	if (cellmap == nil || str == nil)
+		# nothing to do
+		return;
+
+	# normalize pt
+	pt.x--;
+
+	f : ref Font;
+	cell := cellS;
+
+	case charset {
+	videotex		=>
+		if (!(attr & attrD))
+			attr &= (fgMask | attrF | attrH | attrW | attrP);
+		if (attr & attrW && attr & attrH) {
+			cell = cellWH;
+			f = fonts;
+		} else if (attr & attrH) {
+			cell = cellH;
+			f = fonth;
+		} else if (attr & attrW) {
+			cell = cellW;
+			f = fontw;
+		} else {
+			f = font;
+		}
+
+	semigraphic	=>
+		f = fontg1;
+		if (attr & attrL) {
+			# convert to "separated"
+			newstr := "";
+			for (ix := 0; ix < len str; ix++)
+				newstr[ix] = str[ix] + 16r80;
+			str = newstr;
+		}
+		# semigraphic charset does not support size / polarity attributes
+		# attrD always set later once field attr established
+		attr &= ~(attrD | attrH | attrW | attrP | attrL);
+
+	french		=>	f = fontfr;
+	american		=>	f = fontusa;
+	*			=>	f = font;
+	}
+
+	update <- = Pause;
+
+	txty := pt.y - (len cell - 1);
+	for (cellix := len cell - 1; cellix >= 0; cellix--) {
+		y := pt.y - cellix;
+
+		if (y < 0)
+			continue;
+		if (y >= nrows)
+			break;
+
+		col0 := y * ncols;
+		colbase := pt.y * ncols;
+
+		if (delims && !(attr & attrD)) {
+			# seek back for a delimiter
+			mask : int;
+			delimattr := modeattr;
+
+			# semigraphics only inherit attrC from current field
+			if (charset == semigraphic)
+				mask = attrC;
+			else
+				mask  = bgMask | attrC | attrL;
+
+			for (ix := pt.x-1; ix >= 0; ix--) {
+				cix := ix + col0;
+				if (cellmap[cix].attr & attrD) {
+					if (cellmap[cix].font == fontg1 && f != fontg1)
+						# don't carry over attrL from semigraphic field
+						mask &= ~attrL;
+
+					delimattr = cellmap[cix].attr;
+					break;
+				}
+			}
+			attr = (attr & ~mask) | (delimattr & mask);
+
+			# semigraphics validate background colour
+			if (charset == semigraphic)
+				attr |= attrD;
+		}
+
+		strlen := len cell[0] * len str;
+		gfxwidth := cellsize.x * strlen;
+		srco := Point(pt.x*cellsize.x, y*cellsize.y);
+
+		if (insert) {
+			# copy existing cells and display to new position
+			if (pt.x + strlen < ncols) {
+				for (destx := ncols -1; destx > pt.x; destx--) {
+					srcx := destx - strlen;
+					if (srcx < 0)
+						break;
+					cellmap[col0 + destx] = cellmap[col0 + srcx];
+				}
+
+				# let draw() do the clipping for us
+				dsto := Point(srco.x + gfxwidth, srco.y);
+				dstr := Rect((dsto.x, srco.y), (ncols * cellsize.x, srco.y + cellsize.y));
+				
+				frames[0].clipr = frames[0].r;
+				frames[1].clipr = frames[1].r;
+				frames[0].draw(dstr, frames[0], nil, srco);
+				frames[1].draw(dstr, frames[1], nil, srco);
+				if (modbbox.dx() == 0)
+					modbbox = dstr;
+				else
+					modbbox = boundingrect(modbbox, dstr);
+			}
+		}
+
+		# copy-in new string
+		x := pt.x;
+		for (strix := 0; x < ncols && strix < len str; strix++) {
+			for (clipix := 0; clipix < len cell[cellix]; (x, clipix) = (x+1, clipix+1)) {
+				if (x < 0)
+					continue;
+				if (x >= ncols)
+					break;
+				cmix := col0 + x;
+				cellmap[cmix].font = f;
+				cellmap[cmix].ch = str[strix];
+				cellmap[cmix].attr = attr;
+				cellmap[cmix].clipmod = cell[cellix][clipix];
+			}
+		}
+
+		# render the new string
+		txto := Point(srco.x, txty * cellsize.y);
+		strr := Rect(srco, (srco.x + gfxwidth, srco.y + cellsize.y));
+		if (strr.max.x > ncols * cellsize.x)
+			strr.max.x = ncols * cellsize.x;
+
+		drawstr(str, f, strr, txto, attr);
+
+		# redraw remainder of line until find cell not needing redraw
+
+		# this could be optimised by
+		# spotting strings with same attrs, font and clipmod pairs
+		# and write out whole string rather than processing
+		# a char at a time
+
+		attr2 := attr;
+		mask := bgMask | attrC | attrL;
+		s := "";
+		for (; delims && x < ncols; x++) {
+			if (x < 0)
+				continue;
+			newattr := cellmap[col0 + x].attr;
+
+			if (cellmap[col0 + x].font == fontg1) {
+				# semigraphics act as bg colour delimiter
+				attr2 = (attr2 & ~bgMask) | (newattr & bgMask);
+				mask &= ~attrL;
+			} else
+				if (newattr & attrD)
+					break;
+
+			if ((attr2 & mask) == (newattr & mask))
+				break;
+			newattr = (newattr & ~mask) | (attr2 & mask);
+			cellmap[col0 + x].attr = newattr;
+			s[0] = cellmap[col0 + x].ch;
+			(cx, cy) := cellmap[col0 + x].clipmod;
+			f2 := cellmap[col0 + x].font;
+
+			cellpos := Point(x * cellsize.x, y * cellsize.y);
+			clipr := Rect(cellpos, cellpos.add(Point(cellsize.x, cellsize.y)));
+			drawpt := cellpos.sub(Point(cx*cellsize.x, cy*cellsize.y));
+			drawstr(s, f2, clipr, drawpt, newattr);
+		}
+	}
+	update <- = Continue;
+}
+
+Scroll(topline, nlines : int)
+{
+	if (cellmap == nil || nlines == 0)
+		return;
+
+	blankr : Rect;
+	scr := Rect((0,topline * cellsize.y), (ncols * cellsize.x, nrows * cellsize.y));
+
+	update <- = Pause;
+
+	frames[0].clipr = scr;
+	frames[1].clipr = scr;
+	dstr := scr.subpt(Point(0, nlines * cellsize.y));
+
+	frames[0].draw(dstr, frames[0], nil, frames[0].clipr.min);
+	frames[1].draw(dstr, frames[1], nil, frames[1].clipr.min);
+
+	if (nlines > 0) {
+		# scroll up - copy up from top
+		if (nlines > nrows - topline)
+			nlines = nrows - topline;
+		for (y := nlines + topline; y < nrows; y++) {
+			srccol0 := y * ncols;
+			dstcol0 := (y - nlines) * ncols;
+			cellmap[dstcol0:] = cellmap[srccol0:srccol0+ncols];
+		}
+		for (y = nrows - nlines; y < nrows; y++) {
+			col0 := y * ncols;
+			cellmap[col0:] = blankrow;
+		}
+		blankr = Rect(Point(0, scr.max.y - (nlines * cellsize.y)), scr.max);
+	} else {
+		# scroll down - copy down from bottom
+		nlines = -nlines;
+		if (nlines > nrows - topline)
+			nlines = nrows - topline;
+		for (y := (nrows - 1) - nlines; y >= topline; y--) {
+			srccol0 := y * ncols;
+			dstcol0 := (y + nlines) * ncols;
+			cellmap[dstcol0:] = cellmap[srccol0:srccol0+ncols];
+		}
+		for (y = topline; y < nlines; y++) {
+			col0 := y * ncols;
+			cellmap[col0:] = blankrow;
+		}
+		blankr = Rect(scr.min, (scr.max.x, scr.min.y + (nlines * cellsize.y)));
+	}
+	frames[0].draw(blankr, colours[0], nil, Point(0,0));
+	frames[1].draw(blankr, colours[0], nil, Point(0,0));
+	if (modbbox.dx()  == 0)
+		modbbox = scr;
+	else
+		modbbox = boundingrect(modbbox, scr);
+	update <- = Continue;
+}
+
+Reveal(show : int)
+{
+	showC = show;
+	if (cellmap == nil)
+		return;
+
+	update <- = Pause;
+	for (y := 0; y < nrows; y++) {
+		col0 := y * ncols;
+		for (x := 0; x < ncols; x++) {
+			attr := cellmap[col0+x].attr;
+			if (!(attr & attrC))
+				continue;
+
+			s := "";
+			s[0] = cellmap[col0 + x].ch;
+			(cx, cy) := cellmap[col0 + x].clipmod;
+			f := cellmap[col0 + x].font;
+			cellpos := Point(x * cellsize.x, y * cellsize.y);
+			clipr := Rect(cellpos, cellpos.add(Point(cellsize.x, cellsize.y)));
+			drawpt := cellpos.sub(Point(cx*cellsize.x, cy*cellsize.y));
+
+			drawstr(s, f, clipr, drawpt, attr);
+		}
+	}
+	update <- = Continue;
+}
+
+# expects that pt.x already normalized
+wordchar(pt : Point) : int
+{
+	if (pt.x < 0 || pt.x >= ncols)
+		return 0;
+	if (pt.y < 0 || pt.y >= nrows)
+		return 0;
+
+	col0 := pt.y * ncols;
+	c := cellmap[col0 + pt.x];
+
+	if (c.attr & attrC && !showC)
+		# don't let clicking on screen 'reveal' concealed chars!
+		return 0;
+
+	if (c.font == fontg1)
+		return 0;
+
+	if (c.attr & attrW) {
+		# check for both parts of character
+		(modx, nil) := c.clipmod;
+		if (modx == 1) {
+			# rhs of char - check lhs is the same
+			if (pt.x <= 0)
+				return 0;
+			lhc := cellmap[col0 + pt.x-1];
+			(lhmodx, nil) := lhc.clipmod;
+			if (!((lhc.attr & attrW) && (lhc.font == c.font) && (lhc.ch == c.ch) && (lhmodx == 0)))
+				return 0;
+		} else {
+			# lhs of char - check rhs is the same
+			if (pt.x >= ncols - 1)
+				return 0;
+			rhc := cellmap[col0 + pt.x + 1];
+			(rhmodx, nil) := rhc.clipmod;
+			if (!((rhc.attr & attrW) && (rhc.font == c.font) && (rhc.ch == c.ch) && (rhmodx == 1)))
+				return 0;
+		}
+	}
+	if (c.ch >= 16r30 && c.ch <= 16r39)
+		# digits
+		return 1;
+	if (c.ch >= 16r41 && c.ch <= 16r5a)
+		# capitals
+		return 1;
+	if (c.ch >= 16r61 && c.ch <= 16r7a)
+		# lowercase
+		return 1;
+	if (c.ch == '*' || c.ch == '/')
+		return 1;
+	return 0;
+}
+
+GetWord(gfxpt : Point) : string
+{
+	if (cellmap == nil)
+		return nil;
+
+	scr := Rect((0,0), (ncols * cellsize.x, nrows * cellsize.y));
+	gfxpt = gfxpt.sub(winoff);
+
+	if (!gfxpt.in(scr))
+		return nil;
+
+	x := gfxpt.x / cellsize.x;
+	y := gfxpt.y / cellsize.y;
+	col0 := y * ncols;
+
+	s := "";
+
+	# seek back
+	for (sx := x; sx >= 0; sx--)
+		if (!wordchar(Point(sx, y)))
+			break;
+
+	if (sx++ == x)
+		return nil;
+
+	# seek forward, constructing s
+	for (; sx < ncols; sx++) {
+		if (!wordchar(Point(sx, y)))
+			break;
+		c := cellmap[col0 + sx];
+		s[len s] = c.ch;
+		if (c.attr & attrW)
+			sx++;
+	}
+	return s;
+}
+
+Refresh()
+{
+	if (window == nil || modbbox.dx() == 0)
+		return;
+
+	if (update != nil)
+		update <- = Redraw;
+}
+
+framecolours(attr : int) : (ref Image, ref Image, ref Image, ref Image)
+{
+	fg : ref Image;
+	fgcol := attr & fgMask;
+	if (fgcol == fgWhite && attr & attrB)
+		fg = bright;
+	else
+		fg = colours[fgcol / fgBase];
+
+	bg : ref Image;
+	bgcol := attr & bgMask;
+	if (bgcol == bgWhite && attr & attrB)
+		bg = bright;
+	else
+		bg = colours[bgcol / bgBase];
+
+	(fg0, fg1) := (fg, fg);
+	(bg0, bg1) := (bg, bg);
+
+	if (attr & attrP)
+		(fg0, bg0, fg1, bg1) = (bg1, fg1, bg0, fg0);
+
+	if (attr & attrF) {
+		fg0 = fg;
+		fg1 = bg;
+	}
+
+	if ((attr & attrC) && !showC)
+		(fg0, fg1) = (bg0, bg1);
+	return (fg0, bg0, fg1, bg1);
+}
+
+kill(pid : int)
+{
+	prog := "/prog/" + string pid + "/ctl";
+	fd := sys->open(prog, Sys->OWRITE);
+	if (fd != nil) {
+		cmd := array of byte "kill";
+		sys->write(fd, cmd, len cmd);
+	}
+}
+
+timer(ms : int, pc, tick : chan of int)
+{
+	pc <- = sys->pctl(0, nil);
+	for (;;) {
+		sys->sleep(ms);
+		tick <- = 1;
+	}
+}
+
+# Update() commands
+Redraw, Pause, Continue, CursorSet, QuitUpdate : con iota;
+
+Update(cmd : chan of int)
+{
+	flashtick := chan of int;
+	cursortick := chan of int;
+	pc := chan of int;
+	spawn timer(1000, pc, flashtick);
+	flashpid := <- pc;
+	spawn timer(500, pc, cursortick);
+	cursorpid := <- pc;
+
+	cursor	: Point;
+	showcursor := 0;
+	cursoron	:= 0;
+	quit		:= 0;
+	nultick	:= chan of int;
+	flashchan	:= nultick;
+	pcount	:= 1;
+	fgframe	:= 0;
+
+	for (;!quit ;) alt {
+	c := <- cmd =>
+		case c {
+		Redraw =>
+			frames[0].clipr = frames[0].r;
+			frames[1].clipr = frames[1].r;
+			r := modbbox.addpt(winoff);
+			window.draw(r.addpt(window.r.min), frames[fgframe], nil, modbbox.min);
+			if (showcursor && cursoron)
+				drawcursor(cursor, fgframe, 1);
+			modbbox = Rect((0,0),(0,0));
+
+		Pause =>
+			if (pcount++ == 0)
+				flashchan = nultick;
+
+		Continue =>
+			pcount--;
+			if (pcount == 0)
+				flashchan = flashtick;
+
+		QuitUpdate =>
+			quit++;
+
+		CursorSet =>
+			frames[0].clipr = frames[0].r;
+			frames[1].clipr = frames[1].r;
+			if (showcursor && cursoron)
+				drawcursor(cursor, fgframe, 0);
+			cursoron = 0;
+			if (curpos.x < 0 || curpos.x >= ncols || curpos.y < 0  || curpos.y >= nrows)
+				showcursor = 0;
+			else {
+				cursor = curpos;
+				showcursor = 1;
+				drawcursor(cursor, fgframe, 1);
+				cursoron = 1;
+			}
+		}
+
+	<- flashchan =>
+		# flip displays...
+		fgframe = (fgframe + 1 ) % 2;
+		modbbox = Rect((0,0),(0,0));
+		frames[0].clipr = frames[0].r;
+		frames[1].clipr = frames[1].r;
+		window.draw(window.r.addpt(winoff), frames[fgframe], nil, Point(0,0));
+		if (showcursor && cursoron)
+			drawcursor(cursor, fgframe, 1);
+
+	<- cursortick =>
+		if (showcursor) {
+			cursoron = !cursoron;
+			drawcursor(cursor, fgframe, cursoron);
+		}
+	}
+	kill(flashpid);
+	kill(cursorpid);
+}
+
+
+drawstr(s : string, f : ref Font, clipr : Rect, drawpt : Point, attr : int)
+{
+	(fg0, bg0, fg1, bg1) := framecolours(attr);
+	frames[0].clipr = clipr;
+	frames[1].clipr = clipr;
+	frames[0].draw(clipr, bg0, nil, Point(0,0));
+	frames[1].draw(clipr, bg1, nil, Point(0,0));
+	ulrect : Rect;
+	ul := (attr & attrL) && ! (attr & attrD);
+
+	if (f != nil) {
+		if (ul)
+			ulrect = Rect((drawpt.x, drawpt.y + f.height - ulheight), (drawpt.x + clipr.dx(), drawpt.y + f.height));
+		if (fg0 != bg0) {
+			frames[0].text(drawpt, fg0, Point(0,0), f, s);
+			if (ul)
+				frames[0].draw(ulrect, fg0, nil, Point(0,0));
+		}
+		if (fg1 != bg1) {
+			frames[1].text(drawpt, fg1, Point(0,0), f, s);
+			if (ul)
+				frames[1].draw(ulrect, fg1, nil, Point(0,0));
+		}
+	}
+	if (modbbox.dx() == 0)
+		modbbox = clipr;
+	else
+		modbbox = boundingrect(modbbox, clipr);
+}
+
+boundingrect(r1, r2 : Rect) : Rect
+{
+	if (r2.min.x < r1.min.x)
+		r1.min.x = r2.min.x;
+	if (r2.min.y < r1.min.y)
+		r1.min.y = r2.min.y;
+	if (r2.max.x > r1.max.x)
+		r1.max.x = r2.max.x;
+	if (r2.max.y > r1.max.y)
+		r1.max.y = r2.max.y;
+	return r1;
+}
+
+drawcursor(pt : Point, srcix, show : int)
+{
+	col0 := pt.y * ncols;
+	c := cellmap[col0 + pt.x];
+	s := "";
+
+	s[0] = c.ch;
+	(cx, cy) := c.clipmod;
+	cellpos := Point(pt.x * cellsize.x, pt.y * cellsize.y);
+	clipr := Rect(cellpos, cellpos.add(Point(cellsize.x, cellsize.y)));
+	clipr = clipr.addpt(winoff);
+	clipr = clipr.addpt(window.r.min);
+
+	drawpt := cellpos.sub(Point(cx*cellsize.x, cy*cellsize.y));
+	drawpt = drawpt.add(winoff);
+	drawpt = drawpt.add(window.r.min);
+
+	if (!show) {
+		# copy from appropriate frame buffer
+		window.draw(clipr, frames[srcix], nil, cellpos);
+		return;
+	}
+
+	# invert colours
+	attr := c.attr ^ (fgMask | bgMask);
+
+	fg, bg : ref Image;
+	f := c.font;
+	if (srcix == 0)
+		(fg, bg, nil, nil) = framecolours(attr);
+	else
+		(nil, nil, fg, bg) = framecolours(attr);
+
+	prevclipr := window.clipr;
+	window.clipr = clipr;
+
+	window.draw(clipr, bg, nil, Point(0,0));
+	ulrect : Rect;
+	ul := (attr & attrL) && ! (attr & attrD);
+
+	if (f != nil) {
+		if (ul)
+			ulrect = Rect((drawpt.x, drawpt.y + f.height - ulheight), (drawpt.x + clipr.dx(), drawpt.y + f.height));
+		if (fg != bg) {
+			window.text(drawpt, fg, Point(0,0), f, s);
+			if (ul)
+				window.draw(ulrect, fg, nil, Point(0,0));
+		}
+	}
+	window.clipr = prevclipr;
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/examples/minitel/mdisplay.m	Wed Mar 13 02:31:57 2019
@@ -0,0 +1,115 @@
+#
+# Minitel display handling module
+# 
+# © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+MDisplay: module
+{
+
+	PATH:	con "/dis/wm/minitel/mdisplay.dis";
+
+	# Available character sets
+	videotex, semigraphic, french, american : con iota;
+
+	# Fill() attributes bit mask
+	#
+	# DL CFPH WBbb bfff
+	#
+	# D		= Delimiter		(set "serial" attributes for rest of line)
+	# L		= Lining			(underlined text & "separated" graphics)
+	# C		= Concealing
+	# F		= Flashing
+	# P		= polarity			(1 = "inverse")
+	# H		= double height
+	# W		= double width		(set H+W for double size)
+	# B		= bright			(0: fgwhite=lt.grey, 1: fgwhite=white)
+	# bbb	= background colour
+	# fff		= foreground colour
+
+	fgBase	: con 8r001;
+	bgBase	: con 8r010;
+	attrBase	: con 8r100;
+
+	fgMask	: con 8r007;
+	bgMask	: con 8r070;
+	attrMask	: con ~0 ^ (fgMask | bgMask);
+
+	fgBlack, fgBlue, fgRed, fgMagenta,
+	fgGreen, fgCyan, fgYellow, fgWhite : con iota * fgBase;
+
+	bgBlack, bgBlue, bgRed, bgMagenta,
+	bgGreen, bgCyan, bgYellow, bgWhite : con iota * bgBase;
+
+	attrB, attrW, attrH, attrP, attrF, attrC, attrL, attrD : con attrBase << iota;
+
+	#
+	# Init (ctxt) : string
+	# 	performs general module initialisation
+	# 	creates the display window of size/position r using the
+	#	given display context.
+	# 	spawns refresh thread
+	# 	returns reason for error, or nil on success
+	#
+	# Mode(rect, width, height, ulheight, delims, fontpath) : (string, ref Draw->Image)
+	# 	set/reset display to given rectangle and character grid size
+	#	ulheight == underline height from bottom of character cell
+	#	if delims != 0 then "field" attrs for Put() are derived from
+	#	preceding delimiter otherwise Put() attrs are taken as is
+	#
+	#  	load fonts:
+	#		<fontpath>		videotex
+	#		<fontpath>w		videotex double width
+	#		<fontpath>h		videotex double height
+	#		<fontpath>s		videotex double size
+	#		<fontpath>g1		videotex semigraphics
+	#		<fontpath>fr		french character set
+	#		<fontpath>usa		american character set
+	# 	Note:
+	#	charset g2 is not directly supported, instead the symbols
+	#	of g2 that do not appear in g0 (standard videotex charset)
+	#	are available in videotex font using unicode char codes.
+	#	Therefore controlling s/w must map g2 codes to unicode.
+	#
+	# Cursor(pt)
+	#	move cursor to given position
+	#	row number (y) is 0 based
+	#	column number (x) is 1 based
+	#	move cursor off-screen to hide
+	#
+	# Put(str, pt, charset, attr, insert)
+	#	render string str at position pt in the given character set
+	#	using specified attributes.
+	#	if insert is non-zero,  all characters from given position to end
+	#	of line are moved right by len str positions.
+	#
+	# Scroll(topline, nlines)
+	#	move the whole displayby nlines (+ve = scroll up).
+	#	exposed lines of display are set to spaces rendered with
+	#	the current mode attribute flags.
+	#	scroll region is from topline to bottom of display
+	#
+	# Reveal(reveal)
+	#	reveal/hide all chars affected by Concealing attribute.
+	#
+	# Refresh()
+	#	force screen update
+	#
+	# GetWord(pt) : string
+	#	returns on-screen word at given graphics co-ords
+	#	returns nil if blank or semigraphic charset at location
+	#
+	# Quit()
+	#	undo Init()
+	
+
+	Init		: fn (ctxt : ref Draw->Context) : string;
+	Mode	: fn (r : Draw->Rect, width, height, ulh, attr : int, fontpath : string) : (string, ref Draw->Image);
+	Cursor	: fn (pt : Draw->Point);
+	Put		: fn (str : string, pt : Draw->Point, chset, attr, insert : int);
+	Scroll	: fn (topline, nlines : int);
+	Reveal	: fn (reveal : int);
+	Refresh	: fn ();
+	GetWord	: fn (gfxpt : Draw->Point) : string;
+	Quit		: fn ();
+};
Binary files /dev/null and b/fonts/courier/latin1.5 differ
Binary files /dev/null and b/fonts/lucm/currency.9 differ
Binary files /dev/null and b/fonts/lucm/genpunc.9 differ
Binary files /dev/null and b/fonts/lucm/greek.9 differ
Binary files /dev/null and b/fonts/lucm/ipa.9 differ
Binary files /dev/null and b/fonts/lucm/latin1.9 differ
Binary files /dev/null and b/fonts/lucm/latineur.9 differ
Binary files /dev/null and b/fonts/lucm/supsub.9 differ
Binary files /dev/null and b/fonts/misc/cyrillic.9 differ
Binary files /dev/null and b/fonts/misc/genpunc.8 differ
Binary files /dev/null and b/fonts/misc/genpunc.9 differ
Binary files /dev/null and b/fonts/misc/greek.8 differ
Binary files /dev/null and b/fonts/misc/ipa.8 differ
Binary files /dev/null and b/fonts/misc/letterlike.8 differ
Binary files /dev/null and b/fonts/misc/numbforms.9 differ
Binary files /dev/null and b/icons/tk/back.9 differ
Binary files /dev/null and b/icons/tk/forward.9 differ