xref: /inferno-os/appl/cmd/install/updatelog.b (revision 37da2899f40661e3e9631e497da8dc59b971cbd0)
1*37da2899SCharles.Forsythimplement Updatelog;
2*37da2899SCharles.Forsyth
3*37da2899SCharles.Forsythinclude "sys.m";
4*37da2899SCharles.Forsyth	sys: Sys;
5*37da2899SCharles.Forsyth
6*37da2899SCharles.Forsythinclude "draw.m";
7*37da2899SCharles.Forsyth
8*37da2899SCharles.Forsythinclude "bufio.m";
9*37da2899SCharles.Forsyth	bufio: Bufio;
10*37da2899SCharles.Forsyth	Iobuf: import bufio;
11*37da2899SCharles.Forsyth
12*37da2899SCharles.Forsythinclude "daytime.m";
13*37da2899SCharles.Forsyth	daytime: Daytime;
14*37da2899SCharles.Forsyth
15*37da2899SCharles.Forsythinclude "string.m";
16*37da2899SCharles.Forsyth	str: String;
17*37da2899SCharles.Forsyth
18*37da2899SCharles.Forsythinclude "keyring.m";
19*37da2899SCharles.Forsyth	kr: Keyring;
20*37da2899SCharles.Forsyth
21*37da2899SCharles.Forsythinclude "logs.m";
22*37da2899SCharles.Forsyth	logs: Logs;
23*37da2899SCharles.Forsyth	Db, Entry, Byname, Byseq: import logs;
24*37da2899SCharles.Forsyth	S, mkpath: import logs;
25*37da2899SCharles.Forsyth	Log: type Entry;
26*37da2899SCharles.Forsyth
27*37da2899SCharles.Forsythinclude "fsproto.m";
28*37da2899SCharles.Forsyth	fsproto: FSproto;
29*37da2899SCharles.Forsyth	Direntry: import fsproto;
30*37da2899SCharles.Forsyth
31*37da2899SCharles.Forsythinclude "arg.m";
32*37da2899SCharles.Forsyth
33*37da2899SCharles.ForsythUpdatelog: module
34*37da2899SCharles.Forsyth{
35*37da2899SCharles.Forsyth	init:	fn(nil: ref Draw->Context, nil: list of string);
36*37da2899SCharles.Forsyth};
37*37da2899SCharles.Forsyth
38*37da2899SCharles.Forsythnow: int;
39*37da2899SCharles.Forsythgen := 0;
40*37da2899SCharles.Forsythchangesonly := 0;
41*37da2899SCharles.Forsythuid: string;
42*37da2899SCharles.Forsythgid: string;
43*37da2899SCharles.Forsythdebug := 0;
44*37da2899SCharles.Forsythstate: ref Db;
45*37da2899SCharles.Forsythrootdir := ".";
46*37da2899SCharles.Forsythscanonly: list of string;
47*37da2899SCharles.Forsythexclude: list of string;
48*37da2899SCharles.Forsythsums := 0;
49*37da2899SCharles.Forsythstderr: ref Sys->FD;
50*37da2899SCharles.ForsythSeen: con 1<<31;
51*37da2899SCharles.Forsythbout: ref Iobuf;
52*37da2899SCharles.Forsyth
53*37da2899SCharles.Forsythinit(nil: ref Draw->Context, args: list of string)
54*37da2899SCharles.Forsyth{
55*37da2899SCharles.Forsyth	sys = load Sys Sys->PATH;
56*37da2899SCharles.Forsyth	bufio = load Bufio Bufio->PATH;
57*37da2899SCharles.Forsyth	ensure(bufio, Bufio->PATH);
58*37da2899SCharles.Forsyth	fsproto = load FSproto FSproto->PATH;
59*37da2899SCharles.Forsyth	ensure(fsproto, FSproto->PATH);
60*37da2899SCharles.Forsyth	daytime = load Daytime Daytime->PATH;
61*37da2899SCharles.Forsyth	ensure(daytime, Daytime->PATH);
62*37da2899SCharles.Forsyth	str = load String String->PATH;
63*37da2899SCharles.Forsyth	ensure(str, String->PATH);
64*37da2899SCharles.Forsyth	logs = load Logs Logs->PATH;
65*37da2899SCharles.Forsyth	ensure(logs, Logs->PATH);
66*37da2899SCharles.Forsyth	kr = load Keyring Keyring->PATH;
67*37da2899SCharles.Forsyth	ensure(kr, Keyring->PATH);
68*37da2899SCharles.Forsyth
69*37da2899SCharles.Forsyth	arg := load Arg Arg->PATH;
70*37da2899SCharles.Forsyth	if(arg == nil)
71*37da2899SCharles.Forsyth		error(sys->sprint("can't load %s: %r", Arg->PATH));
72*37da2899SCharles.Forsyth
73*37da2899SCharles.Forsyth	protofile := "/lib/proto/all";
74*37da2899SCharles.Forsyth	arg->init(args);
75*37da2899SCharles.Forsyth	arg->setusage("updatelog [-p proto] [-r root] [-t now gen] [-c] [-x path] x.log [path ...]");
76*37da2899SCharles.Forsyth	while((o := arg->opt()) != 0)
77*37da2899SCharles.Forsyth		case o {
78*37da2899SCharles.Forsyth		'D' =>
79*37da2899SCharles.Forsyth			debug = 1;
80*37da2899SCharles.Forsyth		'p' =>
81*37da2899SCharles.Forsyth			protofile = arg->earg();
82*37da2899SCharles.Forsyth		'r' =>
83*37da2899SCharles.Forsyth			rootdir = arg->earg();
84*37da2899SCharles.Forsyth		'c' =>
85*37da2899SCharles.Forsyth			changesonly = 1;
86*37da2899SCharles.Forsyth		'u' =>
87*37da2899SCharles.Forsyth			uid = arg->earg();
88*37da2899SCharles.Forsyth		'g' =>
89*37da2899SCharles.Forsyth			gid = arg->earg();
90*37da2899SCharles.Forsyth		's' =>
91*37da2899SCharles.Forsyth			sums = 1;
92*37da2899SCharles.Forsyth		't' =>
93*37da2899SCharles.Forsyth			now = int arg->earg();
94*37da2899SCharles.Forsyth			gen = int arg->earg();
95*37da2899SCharles.Forsyth		'x' =>
96*37da2899SCharles.Forsyth			s := arg->earg();
97*37da2899SCharles.Forsyth			exclude = trimpath(s) :: exclude;
98*37da2899SCharles.Forsyth		* =>
99*37da2899SCharles.Forsyth			arg->usage();
100*37da2899SCharles.Forsyth		}
101*37da2899SCharles.Forsyth	args = arg->argv();
102*37da2899SCharles.Forsyth	if(args == nil)
103*37da2899SCharles.Forsyth		arg->usage();
104*37da2899SCharles.Forsyth	arg = nil;
105*37da2899SCharles.Forsyth
106*37da2899SCharles.Forsyth	stderr = sys->fildes(2);
107*37da2899SCharles.Forsyth	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
108*37da2899SCharles.Forsyth
109*37da2899SCharles.Forsyth	fsproto->init();
110*37da2899SCharles.Forsyth	logs->init(bufio);
111*37da2899SCharles.Forsyth
112*37da2899SCharles.Forsyth	logfile := hd args;
113*37da2899SCharles.Forsyth	while((args = tl args) != nil)
114*37da2899SCharles.Forsyth		scanonly = trimpath(hd args) :: scanonly;
115*37da2899SCharles.Forsyth	checkroot(rootdir, "replica root");
116*37da2899SCharles.Forsyth
117*37da2899SCharles.Forsyth	state = Db.new("server state");
118*37da2899SCharles.Forsyth
119*37da2899SCharles.Forsyth	#
120*37da2899SCharles.Forsyth	# replay log to rebuild server state
121*37da2899SCharles.Forsyth	#
122*37da2899SCharles.Forsyth	logfd := sys->open(logfile, Sys->OREAD);
123*37da2899SCharles.Forsyth	if(logfd == nil)
124*37da2899SCharles.Forsyth		error(sys->sprint("can't open %s: %r", logfile));
125*37da2899SCharles.Forsyth	f := bufio->fopen(logfd, Sys->OREAD);
126*37da2899SCharles.Forsyth	if(f == nil)
127*37da2899SCharles.Forsyth		error(sys->sprint("can't open %s: %r", logfile));
128*37da2899SCharles.Forsyth	while((log := readlog(f)) != nil)
129*37da2899SCharles.Forsyth		replaylog(state, log);
130*37da2899SCharles.Forsyth
131*37da2899SCharles.Forsyth	#
132*37da2899SCharles.Forsyth	# walk the set of names produced by the proto file, comparing against the server state
133*37da2899SCharles.Forsyth	#
134*37da2899SCharles.Forsyth	now = daytime->now();
135*37da2899SCharles.Forsyth	doproto(rootdir, protofile);
136*37da2899SCharles.Forsyth
137*37da2899SCharles.Forsyth	if(changesonly){
138*37da2899SCharles.Forsyth		bout.flush();
139*37da2899SCharles.Forsyth		exit;
140*37da2899SCharles.Forsyth	}
141*37da2899SCharles.Forsyth
142*37da2899SCharles.Forsyth	#
143*37da2899SCharles.Forsyth	# names in the original state that we didn't see in the walk must have been removed:
144*37da2899SCharles.Forsyth	# print 'd' log entries for them, in reverse lexicographic order (children before parents)
145*37da2899SCharles.Forsyth	#
146*37da2899SCharles.Forsyth	state.sort(Logs->Byname);
147*37da2899SCharles.Forsyth	for(i := state.nstate; --i >= 0;){
148*37da2899SCharles.Forsyth		e := state.state[i];
149*37da2899SCharles.Forsyth		if((e.x & Seen) == 0 && considered(e.path)){
150*37da2899SCharles.Forsyth			change('d', e, e.seq, e.d, e.path, e.serverpath, e.contents);	# TO DO: content
151*37da2899SCharles.Forsyth			if(debug)
152*37da2899SCharles.Forsyth				sys->fprint(sys->fildes(2), "remove %q\n", e.path);
153*37da2899SCharles.Forsyth		}
154*37da2899SCharles.Forsyth	}
155*37da2899SCharles.Forsyth	bout.flush();
156*37da2899SCharles.Forsyth}
157*37da2899SCharles.Forsyth
158*37da2899SCharles.Forsythensure[T](m: T, path: string)
159*37da2899SCharles.Forsyth{
160*37da2899SCharles.Forsyth	if(m == nil)
161*37da2899SCharles.Forsyth		error(sys->sprint("can't load %s: %r", path));
162*37da2899SCharles.Forsyth}
163*37da2899SCharles.Forsyth
164*37da2899SCharles.Forsythcheckroot(dir: string, what: string)
165*37da2899SCharles.Forsyth{
166*37da2899SCharles.Forsyth	(ok, d) := sys->stat(dir);
167*37da2899SCharles.Forsyth	if(ok < 0)
168*37da2899SCharles.Forsyth		error(sys->sprint("can't stat %s %q: %r", what, dir));
169*37da2899SCharles.Forsyth	if((d.mode & Sys->DMDIR) == 0)
170*37da2899SCharles.Forsyth		error(sys->sprint("%s %q: not a directory", what, dir));
171*37da2899SCharles.Forsyth}
172*37da2899SCharles.Forsyth
173*37da2899SCharles.Forsythconsidered(s: string): int
174*37da2899SCharles.Forsyth{
175*37da2899SCharles.Forsyth	if(scanonly != nil && !islisted(s, scanonly))
176*37da2899SCharles.Forsyth		return 0;
177*37da2899SCharles.Forsyth	return exclude == nil || !islisted(s, exclude);
178*37da2899SCharles.Forsyth}
179*37da2899SCharles.Forsyth
180*37da2899SCharles.Forsythreadlog(in: ref Iobuf): ref Log
181*37da2899SCharles.Forsyth{
182*37da2899SCharles.Forsyth	(e, err) := Entry.read(in);
183*37da2899SCharles.Forsyth	if(err != nil)
184*37da2899SCharles.Forsyth		error(err);
185*37da2899SCharles.Forsyth	return e;
186*37da2899SCharles.Forsyth}
187*37da2899SCharles.Forsyth
188*37da2899SCharles.Forsyth#
189*37da2899SCharles.Forsyth# replay a log to reach the state wrt files previously taken from the server
190*37da2899SCharles.Forsyth#
191*37da2899SCharles.Forsythreplaylog(db: ref Db, log: ref Log)
192*37da2899SCharles.Forsyth{
193*37da2899SCharles.Forsyth	e := db.look(log.path);
194*37da2899SCharles.Forsyth	indb := e != nil && !e.removed();
195*37da2899SCharles.Forsyth	case log.action {
196*37da2899SCharles.Forsyth	'a' =>	# add new file
197*37da2899SCharles.Forsyth		if(indb){
198*37da2899SCharles.Forsyth			note(sys->sprint("%q duplicate create", log.path));
199*37da2899SCharles.Forsyth			return;
200*37da2899SCharles.Forsyth		}
201*37da2899SCharles.Forsyth	'c' =>	# contents
202*37da2899SCharles.Forsyth		if(!indb){
203*37da2899SCharles.Forsyth			note(sys->sprint("%q contents but no entry", log.path));
204*37da2899SCharles.Forsyth			return;
205*37da2899SCharles.Forsyth		}
206*37da2899SCharles.Forsyth	'd' =>	# delete
207*37da2899SCharles.Forsyth		if(!indb){
208*37da2899SCharles.Forsyth			note(sys->sprint("%q deleted but no entry", log.path));
209*37da2899SCharles.Forsyth			return;
210*37da2899SCharles.Forsyth		}
211*37da2899SCharles.Forsyth		if(e.d.mtime > log.d.mtime){
212*37da2899SCharles.Forsyth			note(sys->sprint("%q deleted but it's newer", log.path));
213*37da2899SCharles.Forsyth			return;
214*37da2899SCharles.Forsyth		}
215*37da2899SCharles.Forsyth	'm' =>	# metadata
216*37da2899SCharles.Forsyth		if(!indb){
217*37da2899SCharles.Forsyth			note(sys->sprint("%q metadata but no entry", log.path));
218*37da2899SCharles.Forsyth			return;
219*37da2899SCharles.Forsyth		}
220*37da2899SCharles.Forsyth	* =>
221*37da2899SCharles.Forsyth		error(sys->sprint("bad log entry: %bd %bd", log.seq>>32, log.seq & big 16rFFFFFFFF));
222*37da2899SCharles.Forsyth	}
223*37da2899SCharles.Forsyth	update(db, e, log);
224*37da2899SCharles.Forsyth}
225*37da2899SCharles.Forsyth
226*37da2899SCharles.Forsyth#
227*37da2899SCharles.Forsyth# update file state e to reflect the effect of the log,
228*37da2899SCharles.Forsyth# creating a new entry if necessary
229*37da2899SCharles.Forsyth#
230*37da2899SCharles.Forsythupdate(db: ref Db, e: ref Entry, log: ref Entry)
231*37da2899SCharles.Forsyth{
232*37da2899SCharles.Forsyth	if(e == nil)
233*37da2899SCharles.Forsyth		e = db.entry(log.seq, log.path, log.d);
234*37da2899SCharles.Forsyth	e.update(log);
235*37da2899SCharles.Forsyth}
236*37da2899SCharles.Forsyth
237*37da2899SCharles.Forsythdoproto(tree: string, protofile: string)
238*37da2899SCharles.Forsyth{
239*37da2899SCharles.Forsyth	entries := chan of Direntry;
240*37da2899SCharles.Forsyth	warnings := chan of (string, string);
241*37da2899SCharles.Forsyth	err := fsproto->readprotofile(protofile, tree, entries, warnings);
242*37da2899SCharles.Forsyth	if(err != nil)
243*37da2899SCharles.Forsyth		error(sys->sprint("can't read %s: %s", protofile, err));
244*37da2899SCharles.Forsyth	for(;;)alt{
245*37da2899SCharles.Forsyth	(old, new, d) := <-entries =>
246*37da2899SCharles.Forsyth		if(d == nil)
247*37da2899SCharles.Forsyth			return;
248*37da2899SCharles.Forsyth		if(debug)
249*37da2899SCharles.Forsyth			sys->fprint(stderr, "old=%q new=%q length=%bd\n", old, new, d.length);
250*37da2899SCharles.Forsyth		while(new != nil && new[0] == '/')
251*37da2899SCharles.Forsyth			new = new[1:];
252*37da2899SCharles.Forsyth		if(!considered(new))
253*37da2899SCharles.Forsyth			continue;
254*37da2899SCharles.Forsyth		if(sums && (d.mode & Sys->DMDIR) == 0)
255*37da2899SCharles.Forsyth			digests := md5sum(old) :: nil;
256*37da2899SCharles.Forsyth		if(uid != nil)
257*37da2899SCharles.Forsyth			d.uid = uid;
258*37da2899SCharles.Forsyth		if(gid != nil)
259*37da2899SCharles.Forsyth			d.gid = gid;
260*37da2899SCharles.Forsyth		old = relative(old, rootdir);
261*37da2899SCharles.Forsyth		db := state.look(new);
262*37da2899SCharles.Forsyth		if(db == nil){
263*37da2899SCharles.Forsyth			if(!changesonly){
264*37da2899SCharles.Forsyth				db = state.entry(nextseq(), new, *d);
265*37da2899SCharles.Forsyth				change('a', db, db.seq, db.d, db.path, old, digests);
266*37da2899SCharles.Forsyth			}
267*37da2899SCharles.Forsyth		}else{
268*37da2899SCharles.Forsyth			if(!samestat(db.d, *d))
269*37da2899SCharles.Forsyth				change('c', db, nextseq(), *d, new, old, digests);
270*37da2899SCharles.Forsyth			if(!samemeta(db.d, *d))
271*37da2899SCharles.Forsyth				change('m', db, nextseq(), *d, new, old, nil);	# need digest?
272*37da2899SCharles.Forsyth		}
273*37da2899SCharles.Forsyth		if(db != nil)
274*37da2899SCharles.Forsyth			db.x |= Seen;
275*37da2899SCharles.Forsyth	(old, msg) := <-warnings =>
276*37da2899SCharles.Forsyth		#if(contains(msg, "entry not found") || contains(msg, "not exist"))
277*37da2899SCharles.Forsyth		#	break;
278*37da2899SCharles.Forsyth		sys->fprint(sys->fildes(2), "updatelog: warning[old=%s]: %s\n", old, msg);
279*37da2899SCharles.Forsyth	}
280*37da2899SCharles.Forsyth}
281*37da2899SCharles.Forsyth
282*37da2899SCharles.Forsythchange(action: int, e: ref Entry, seq: big, d: Sys->Dir, path: string, serverpath: string, digests: list of string)
283*37da2899SCharles.Forsyth{
284*37da2899SCharles.Forsyth	log := ref Entry;
285*37da2899SCharles.Forsyth	log.seq = seq;
286*37da2899SCharles.Forsyth	log.action = action;
287*37da2899SCharles.Forsyth	log.d = d;
288*37da2899SCharles.Forsyth	log.path = path;
289*37da2899SCharles.Forsyth	log.serverpath = serverpath;
290*37da2899SCharles.Forsyth	log.contents = digests;
291*37da2899SCharles.Forsyth	e.update(log);
292*37da2899SCharles.Forsyth	bout.puts(log.logtext()+"\n");
293*37da2899SCharles.Forsyth}
294*37da2899SCharles.Forsyth
295*37da2899SCharles.Forsythsamestat(a: Sys->Dir, b: Sys->Dir): int
296*37da2899SCharles.Forsyth{
297*37da2899SCharles.Forsyth	# doesn't check permission/ownership, does check QTDIR/QTFILE
298*37da2899SCharles.Forsyth	if(a.mode & Sys->DMDIR)
299*37da2899SCharles.Forsyth		return (b.mode & Sys->DMDIR) != 0;
300*37da2899SCharles.Forsyth	return a.length == b.length && a.mtime == b.mtime && a.qid.qtype == b.qid.qtype;	# TO DO: a.name==b.name?
301*37da2899SCharles.Forsyth}
302*37da2899SCharles.Forsyth
303*37da2899SCharles.Forsythsamemeta(a: Sys->Dir, b: Sys->Dir): int
304*37da2899SCharles.Forsyth{
305*37da2899SCharles.Forsyth	return a.mode == b.mode && (uid == nil || a.uid == b.uid) && (gid == nil || a.gid == b.gid) && samestat(a, b);
306*37da2899SCharles.Forsyth}
307*37da2899SCharles.Forsyth
308*37da2899SCharles.Forsythnextseq(): big
309*37da2899SCharles.Forsyth{
310*37da2899SCharles.Forsyth	return (big now << 32) | big gen++;
311*37da2899SCharles.Forsyth}
312*37da2899SCharles.Forsyth
313*37da2899SCharles.Forsytherror(s: string)
314*37da2899SCharles.Forsyth{
315*37da2899SCharles.Forsyth	sys->fprint(sys->fildes(2), "updatelog: %s\n", s);
316*37da2899SCharles.Forsyth	raise "fail:error";
317*37da2899SCharles.Forsyth}
318*37da2899SCharles.Forsyth
319*37da2899SCharles.Forsythnote(s: string)
320*37da2899SCharles.Forsyth{
321*37da2899SCharles.Forsyth	sys->fprint(sys->fildes(2), "updatelog: note: %s\n", s);
322*37da2899SCharles.Forsyth}
323*37da2899SCharles.Forsyth
324*37da2899SCharles.Forsythcontains(s: string, sub: string): int
325*37da2899SCharles.Forsyth{
326*37da2899SCharles.Forsyth	return str->splitstrl(s, sub).t1 != nil;
327*37da2899SCharles.Forsyth}
328*37da2899SCharles.Forsyth
329*37da2899SCharles.Forsythisprefix(a, b: string): int
330*37da2899SCharles.Forsyth{
331*37da2899SCharles.Forsyth	la := len a;
332*37da2899SCharles.Forsyth	lb := len b;
333*37da2899SCharles.Forsyth	if(la > lb)
334*37da2899SCharles.Forsyth		return 0;
335*37da2899SCharles.Forsyth	if(la == lb)
336*37da2899SCharles.Forsyth		return a == b;
337*37da2899SCharles.Forsyth	return a == b[0:la] && b[la] == '/';
338*37da2899SCharles.Forsyth}
339*37da2899SCharles.Forsyth
340*37da2899SCharles.Forsythtrimpath(s: string): string
341*37da2899SCharles.Forsyth{
342*37da2899SCharles.Forsyth	while(len s > 1 && s[len s-1] == '/')
343*37da2899SCharles.Forsyth		s = s[0:len s-1];
344*37da2899SCharles.Forsyth	while(s != nil && s[0] == '/')
345*37da2899SCharles.Forsyth		s = s[1:];
346*37da2899SCharles.Forsyth	return s;
347*37da2899SCharles.Forsyth}
348*37da2899SCharles.Forsyth
349*37da2899SCharles.Forsythrelative(name: string, root: string): string
350*37da2899SCharles.Forsyth{
351*37da2899SCharles.Forsyth	if(root == nil || name == nil)
352*37da2899SCharles.Forsyth		return name;
353*37da2899SCharles.Forsyth	if(isprefix(root, name)){
354*37da2899SCharles.Forsyth		name = name[len root:];
355*37da2899SCharles.Forsyth		while(name != nil && name[0] == '/')
356*37da2899SCharles.Forsyth			name = name[1:];
357*37da2899SCharles.Forsyth	}
358*37da2899SCharles.Forsyth	return name;
359*37da2899SCharles.Forsyth}
360*37da2899SCharles.Forsyth
361*37da2899SCharles.Forsythislisted(s: string, l: list of string): int
362*37da2899SCharles.Forsyth{
363*37da2899SCharles.Forsyth	for(; l != nil; l = tl l)
364*37da2899SCharles.Forsyth		if(isprefix(hd l, s))
365*37da2899SCharles.Forsyth			return 1;
366*37da2899SCharles.Forsyth	return 0;
367*37da2899SCharles.Forsyth}
368*37da2899SCharles.Forsyth
369*37da2899SCharles.Forsythmd5sum(file: string): string
370*37da2899SCharles.Forsyth{
371*37da2899SCharles.Forsyth	fd := sys->open(file, Sys->OREAD);
372*37da2899SCharles.Forsyth	if(fd == nil)
373*37da2899SCharles.Forsyth		error(sys->sprint("can't open %s: %r", file));
374*37da2899SCharles.Forsyth	ds: ref Keyring->DigestState;
375*37da2899SCharles.Forsyth	buf := array[Sys->ATOMICIO] of byte;
376*37da2899SCharles.Forsyth	while((n := sys->read(fd, buf, len buf)) > 0)
377*37da2899SCharles.Forsyth		ds = kr->md5(buf, n, nil, ds);
378*37da2899SCharles.Forsyth	if(n < 0)
379*37da2899SCharles.Forsyth		error(sys->sprint("error reading %s: %r", file));
380*37da2899SCharles.Forsyth	digest := array[Keyring->MD5dlen] of byte;
381*37da2899SCharles.Forsyth	kr->md5(nil, 0, digest, ds);
382*37da2899SCharles.Forsyth	s: string;
383*37da2899SCharles.Forsyth	for(i := 0; i < len digest; i++)
384*37da2899SCharles.Forsyth		s += sys->sprint("%.2ux", int digest[i]);
385*37da2899SCharles.Forsyth	return s;
386*37da2899SCharles.Forsyth}
387