xref: /inferno-os/appl/cmd/diff.b (revision 7ef44d652ae9e5e1f5b3465d73684e4a54de73c0)
1implement Diff;
2
3#	diff - differential file comparison
4#
5#	Uses an algorithm due to Harold Stone, which finds
6#	a pair of longest identical subsequences in the two
7#	files.
8#
9#	The major goal is to generate the match vector J.
10#	J[i] is the index of the line in file1 corresponding
11#	to line i file0. J[i] = 0 if there is no
12#	such line in file1.
13#
14#	Lines are hashed so as to work in core. All potential
15#	matches are located by sorting the lines of each file
16#	on the hash (called value). In particular, this
17#	collects the equivalence classes in file1 together.
18#	Subroutine equiv replaces the value of each line in
19#	file0 by the index of the first element of its
20#	matching equivalence in (the reordered) file1.
21#	To save space equiv squeezes file1 into a single
22#	array member in which the equivalence classes
23#	are simply concatenated, except that their first
24#	members are flagged by changing sign.
25#
26#	Next the indices that point into member are unsorted into
27#	array class according to the original order of file0.
28#
29#	The cleverness lies in routine stone. This marches
30#	through the lines of file0, developing a vector klist
31#	of "k-candidates". At step i a k-candidate is a matched
32#	pair of lines x,y (x in file0 y in file1) such that
33#	there is a common subsequence of lenght k
34#	between the first i lines of file0 and the first y
35#	lines of file1, but there is no such subsequence for
36#	any smaller y. x is the earliest possible mate to y
37#	that occurs in such a subsequence.
38#
39#	Whenever any of the members of the equivalence class of
40#	lines in file1 matable to a line in file0 has serial number
41#	less than the y of some k-candidate, that k-candidate
42#	with the smallest such y is replaced. The new
43#	k-candidate is chained (via pred) to the current
44#	k-1 candidate so that the actual subsequence can
45#	be recovered. When a member has serial number greater
46#	that the y of all k-candidates, the klist is extended.
47#	At the end, the longest subsequence is pulled out
48#	and placed in the array J by unravel.
49#
50#	With J in hand, the matches there recorded are
51#	check'ed against reality to assure that no spurious
52#	matches have crept in due to hashing. If they have,
53#	they are broken, and "jackpot " is recorded--a harmless
54#	matter except that a true match for a spuriously
55#	mated line may now be unnecessarily reported as a change.
56#
57#	Much of the complexity of the program comes simply
58#	from trying to minimize core utilization and
59#	maximize the range of doable problems by dynamically
60#	allocating what is needed and reusing what is not.
61#	The core requirements for problems larger than somewhat
62#	are (in words) 2*length(file0) + length(file1) +
63#	3*(number of k-candidates installed),  typically about
64#	6n words for files of length n.
65#
66#
67
68include "sys.m";
69	sys: Sys;
70
71include "bufio.m";
72	bufio : Bufio;
73Iobuf : import bufio;
74
75include "draw.m";
76	draw: Draw;
77include "readdir.m";
78	readdir : Readdir;
79include "string.m";
80	str : String;
81include "arg.m";
82
83Diff : module
84{
85	init: fn(ctxt: ref Draw->Context, argv: list of string);
86};
87
88stderr: ref Sys->FD;
89
90mode : int;			# '\0', 'e', 'f', 'h'
91bflag : int;			# ignore multiple and trailing blanks
92rflag : int;			# recurse down directory trees
93mflag : int;			# pseudo flag: doing multiple files, one dir
94
95REG,
96BIN: con iota;
97
98HALFINT : con 16;
99Usage : con  "usage: diff [ -efbwr ] file1 ... file2";
100
101cand : adt {
102	x : int;
103	y : int;
104	pred : int;
105};
106
107line : adt {
108	serial : int;
109	value : int;
110};
111
112out : ref Iobuf;
113file := array[2] of array of line;
114sfile := array[2] of array of line;	# shortened by pruning common prefix and suffix
115slen := array[2] of int;
116ilen := array[2] of int;
117pref, suff, clen : int;			# length of prefix and suffix
118firstchange : int;
119clist : array of cand;			# merely a free storage pot for candidates
120J : array of int;			# will be overlaid on class
121ixold, ixnew : array of int;
122input := array[2] of ref Iobuf ;
123file1, file2 : string;
124tmpname := array[] of {"/tmp/diff1", "/tmp/diff2"};
125whichtmp : int;
126anychange := 0;
127
128init(nil: ref Draw->Context, args: list of string)
129{
130	sys = load Sys Sys->PATH;
131	stderr = sys->fildes(2);
132	draw = load Draw Draw->PATH;
133	bufio = load Bufio Bufio->PATH;
134	readdir = load Readdir Readdir->PATH;
135	str = load String String->PATH;
136	if (bufio==nil)
137		fatal(sys->sprint("cannot load %s: %r", Bufio->PATH));
138	if (readdir==nil)
139		fatal(sys->sprint("cannot load %s: %r", Readdir->PATH));
140	if (str==nil)
141		fatal(sys->sprint("cannot load %s: %r", String->PATH));
142	arg := load Arg Arg->PATH;
143	if (arg==nil)
144		fatal(sys->sprint("cannot load %s: %r", Arg->PATH));
145	fsb, tsb : Sys->Dir;
146	arg->init(args);
147	while((o := arg->opt()) != 0)
148		case o {
149		'e' or 'f' =>
150			mode = o;
151		'w' =>
152			bflag = 2;
153		'b' =>
154			bflag = 1;
155		'r' =>
156			rflag = 1;
157		'm' =>
158			mflag = 1;
159		* =>
160			fatal(Usage);
161		}
162	tmp := arg->argv();
163	arg = nil;
164	j := len tmp;
165	if (j < 2)
166		fatal(Usage);
167	arr := array[j] of string;
168	for(i:=0;i<j;i++){
169		arr[i]= hd tmp;
170		tmp = tl tmp;
171	}
172
173	(i,tsb)=sys->stat(arr[j-1]);
174	if (i == -1)
175		fatal(sys->sprint("can't stat %s: %r", arr[j-1]));
176	if (j > 2) {
177		if (!(tsb.qid.qtype&Sys->QTDIR))
178			fatal(Usage);
179		mflag = 1;
180	}
181	else {
182		(i,fsb)=sys->stat(arr[0]);
183		if (i == -1)
184			fatal(sys->sprint("can't stat %s: %r", arr[0]));
185		if ((fsb.qid.qtype&Sys->QTDIR) && (tsb.qid.qtype&Sys->QTDIR))
186			mflag = 1;
187	}
188	out=bufio->fopen(sys->fildes(1),Bufio->OWRITE);
189	for (i = 0; i < j-1; i++) {
190		diff(arr[i], arr[j-1], 0);
191		rmtmpfiles();
192	}
193	rmtmpfiles();
194	out.flush();
195	if (anychange)
196		raise "fail:some";
197}
198
199############################# diffreg from here ....
200
201# shellsort CACM #201
202
203sort(a : array of line, n : int)
204{
205	w : line;
206	m := 0;
207	for (i := 1; i <= n; i *= 2)
208		m = 2*i - 1;
209	for (m /= 2; m != 0; m /= 2) {
210		for (j := 1; j <= n-m ; j++) {
211			ai:=j;
212			aim:=j+m;
213			do {
214				if (a[aim].value > a[ai].value ||
215				   a[aim].value == a[ai].value &&
216				   a[aim].serial > a[ai].serial)
217					break;
218				w = a[ai];
219				a[ai] = a[aim];
220				a[aim] = w;
221				aim=ai;
222				ai-=m;
223			} while (ai > 0 && aim >= ai);
224		}
225	}
226}
227
228unsort(f : array of line, l : int) : array of int
229{
230	i : int;
231	a := array[l+1] of int;
232	for(i=1;i<=l;i++)
233		a[f[i].serial] = f[i].value;
234	return a;
235}
236
237prune()
238{
239	for(pref=0;pref< ilen[0]&&pref< ilen[1]&&
240		file[0][pref+1].value==file[1][pref+1].value;
241		pref++ ) ;
242	for(suff=0;suff< ilen[0]-pref&&suff< ilen[1]-pref&&
243		file[0][ilen[0]-suff].value==file[1][ilen[1]-suff].value;
244		suff++) ;
245	for(j:=0;j<2;j++) {
246		sfile[j] = file[j][pref:];
247		slen[j]= ilen[j]-pref-suff;
248		for(i:=0;i<=slen[j];i++)
249			sfile[j][i].serial = i;
250	}
251}
252
253equiv(a: array of line, n:int , b: array of line, m: int, c : array of int)
254{
255	i := 1;
256	j := 1;
257	while(i<=n && j<=m) {
258		if(a[i].value < b[j].value)
259			a[i++].value = 0;
260		else if(a[i].value == b[j].value)
261			a[i++].value = j;
262		else
263			j++;
264	}
265	while(i <= n)
266		a[i++].value = 0;
267	b[m+1].value = 0; # huh ?
268	j = 1;
269	while(j <= m) {
270		c[j] = -b[j].serial;
271		while(b[j+1].value == b[j].value) {
272			j++;
273			c[j] = b[j].serial;
274		}
275		j++;
276	}
277	c[j] = -1;
278}
279
280newcand(x, y, pred : int) : int
281{
282	if (clen==len clist){
283		q := array[clen*2] of cand;
284		q[0:]=clist;
285		clist= array[clen*2] of cand;
286		clist[0:]=q;
287		q=nil;
288	}
289	clist[clen].x=x;
290	clist[clen].y=y;
291	clist[clen].pred=pred;
292	return clen++;
293}
294
295search(c : array of int, k,y : int) : int
296{
297	if(clist[c[k]].y < y)	# quick look for typical case
298		return k+1;
299	i := 0;
300	j := k+1;
301	while((l:=(i+j)/2) > i) {
302		t := clist[c[l]].y;
303		if(t > y)
304			j = l;
305		else if(t < y)
306			i = l;
307		else
308			return l;
309	}
310	return l+1;
311}
312
313stone(a : array of int ,n : int, b: array of int , c : array of int) : int
314{
315	oldc, oldl, tc, l ,y : int;
316	k := 0;
317	c[0] = newcand(0,0,0);
318	for(i:=1; i<=n; i++) {
319		j := a[i];
320		if(j==0)
321			continue;
322		y = -b[j];
323		oldl = 0;
324		oldc = c[0];
325		do {
326			if(y <= clist[oldc].y)
327				continue;
328			l = search(c, k, y);
329			if(l!=oldl+1)
330				oldc = c[l-1];
331			if(l<=k) {
332				if(clist[c[l]].y <= y)
333					continue;
334				tc = c[l];
335				c[l] = newcand(i,y,oldc);
336				oldc = tc;
337				oldl = l;
338			} else {
339				c[l] = newcand(i,y,oldc);
340				k++;
341				break;
342			}
343		} while((y=b[j+=1]) > 0);
344	}
345	return k;
346}
347
348unravel(p : int)
349{
350	for(i:=0; i<=ilen[0]; i++) {
351		if (i <= pref)
352			J[i] = i;
353		else if (i > ilen[0]-suff)
354			J[i] = i+ ilen[1]-ilen[0];
355		else
356			J[i] = 0;
357	}
358	for(q:=clist[p];q.y!=0;q=clist[q.pred])
359		J[q.x+pref] = q.y+pref;
360}
361
362output()
363{
364	i1: int;
365	m := ilen[0];
366	J[0] = 0;
367	J[m+1] = ilen[1]+1;
368	if (mode != 'e') {
369		for (i0 := 1; i0 <= m; i0 = i1+1) {
370			while (i0 <= m && J[i0] == J[i0-1]+1)
371				i0++;
372			j0 := J[i0-1]+1;
373			i1 = i0-1;
374			while (i1 < m && J[i1+1] == 0)
375				i1++;
376			j1 := J[i1+1]-1;
377			J[i1] = j1;
378			change(i0, i1, j0, j1);
379		}
380	}
381	else {
382		for (i0 := m; i0 >= 1; i0 = i1-1) {
383			while (i0 >= 1 && J[i0] == J[i0+1]-1 && J[i0])
384				i0--;
385			j0 := J[i0+1]-1;
386			i1 = i0+1;
387			while (i1 > 1 && J[i1-1] == 0)
388				i1--;
389			j1 := J[i1-1]+1;
390			J[i1] = j1;
391			change(i1 , i0, j1, j0);
392		}
393	}
394	if (m == 0)
395		change(1, 0, 1, ilen[1]);
396	out.flush();
397}
398
399diffreg(f,t : string)
400{
401	k : int;
402
403	(b0, b0type) := prepare(0, f);
404	if (b0==nil)
405		return;
406	(b1, b1type) := prepare(1, t);
407	if (b1==nil) {
408		b0=nil;
409		return;
410	}
411	if (b0type == BIN || b1type == BIN) {
412		if (cmp(b0, b1)) {
413			out.puts(sys->sprint("Binary files %s %s differ\n", f, t));
414			anychange = 1;
415		}
416		b0 = nil;
417		b1 = nil;
418		return;
419	}
420	clen=0;
421	prune();
422	file[0]=nil;
423	file[1]=nil;
424	sort(sfile[0],slen[0]);
425	sort(sfile[1],slen[1]);
426	member := array[slen[1]+2] of int;
427	equiv(sfile[0], slen[0],sfile[1],slen[1], member);
428	class:=unsort(sfile[0],slen[0]);
429	sfile[0]=nil;
430	sfile[1]=nil;
431	klist := array[slen[0]+2] of int;
432	clist = array[1] of cand;
433	k = stone(class, slen[0], member, klist);
434	J = array[ilen[0]+2] of int;
435	unravel(klist[k]);
436	clist=nil;
437	klist=nil;
438	class=nil;
439	member=nil;
440	ixold = array[ilen[0]+2] of int;
441	ixnew = array[ilen[1]+2] of int;
442
443	b0.seek(big 0, 0);
444	b1.seek(big 0, 0);
445	check(b0, b1);
446	output();
447	ixold=nil;
448	ixnew=nil;
449	b0=nil;
450	b1=nil;
451}
452
453######################## diffio starts here...
454
455
456# hashing has the effect of
457# arranging line in 7-bit bytes and then
458# summing 1-s complement in 16-bit hunks
459
460readhash(bp : ref Iobuf) : int
461{
462	sum := 1;
463	shift := 0;
464	buf := bp.gets('\n');
465	if (buf == nil)
466		return 0;
467	buf = buf[0:len buf -1];
468	p := 0;
469	case bflag {
470		# various types of white space handling
471		0 =>
472			while (p< len buf) {
473				sum += (buf[p] << (shift &= (HALFINT-1)));
474				p++;
475				shift += 7;
476			}
477		1 =>
478
479			 # coalesce multiple white-space
480
481			for (space := 0; p< len buf; p++) {
482				if (buf[p]==' ' || buf[p]=='\t') {
483					space++;
484					continue;
485				}
486				if (space) {
487					shift += 7;
488					space = 0;
489				}
490				sum +=  (buf[p] << (shift &= (HALFINT-1)));
491				p++;
492				shift += 7;
493			}
494		* =>
495
496			 # strip all white-space
497
498			while (p< len buf) {
499				if (buf[p]==' ' || buf[p]=='\t') {
500					p++;
501					continue;
502				}
503				sum +=  (buf[p] << (shift &= (HALFINT-1)));
504				p++;
505				shift += 7;
506			}
507	}
508	return sum;
509}
510
511prepare(i : int, arg : string) : (ref Iobuf, int)
512{
513	h : int;
514	bp := bufio->open(arg,Bufio->OREAD);
515	if (bp==nil) {
516		error(sys->sprint("cannot open %s: %r", arg));
517		return (nil, 0);
518	}
519	buf := array[1024] of byte;
520	n :=bp.read(buf, len buf);
521	str1 := string buf[0:n];
522	for (j:=0;j<len str1 -2;j++)
523		if (str1[j] == Sys->UTFerror)
524			return (bp, BIN);
525	bp.seek(big 0, Sys->SEEKSTART);
526	p := array[4] of line;
527	for (j = 0; h = readhash(bp); p[j].value = h){
528		j++;
529		if (j+3>=len p){
530			newp:=array[len p*2] of line;
531			newp[0:]=p[0:];
532			p=array[len p*2] of line;
533			p=newp;
534			newp=nil;
535		}
536	}
537	ilen[i]=j;
538	file[i] = p;
539	input[i] = bp;
540	if (i == 0) {
541		file1 = arg;
542		firstchange = 0;
543	}
544	else
545		file2 = arg;
546	return (bp, REG);
547}
548
549squishspace(buf : string) : string
550{
551	q:=0;
552	p:=0;
553	for (space := 0; q<len buf; q++) {
554		if (buf[q]==' ' || buf[q]=='\t') {
555			space++;
556			continue;
557		}
558		if (space && bflag == 1) {
559			buf[p] = ' ';
560			p++;
561			space = 0;
562		}
563		buf[p]=buf[q];
564		p++;
565	}
566	buf=buf[0:p];
567	return buf;
568}
569
570
571# need to fix up for unexpected EOF's
572
573ftell(b: ref Iobuf): int
574{
575	return int b.offset();
576}
577
578check(bf, bt : ref Iobuf)
579{
580	fbuf, tbuf : string;
581	f:=1;
582	t:=1;
583	ixold[0] = ixnew[0] = 0;
584	for (; f < ilen[0]; f++) {
585		fbuf = bf.gets('\n');
586		if (fbuf!=nil)
587			fbuf=fbuf[0:len fbuf -1];
588		ixold[f] = ftell(bf);
589		if (J[f] == 0)
590			continue;
591		tbuflen: int;
592		do {
593			tbuf = bt.gets('\n');
594			if (tbuf!=nil)
595				tbuf=tbuf[0:len tbuf -1];
596			tbuflen = len array of byte tbuf;
597			ixnew[t] = ftell(bt);
598		} while (t++ < J[f]);
599		if (bflag) {
600			fbuf = squishspace(fbuf);
601			tbuf = squishspace(tbuf);
602		}
603		if (len fbuf != len tbuf || fbuf!=tbuf)
604			J[f] = 0;
605	}
606	while (t < ilen[1]) {
607		tbuf = bt.gets('\n');
608		if (tbuf!=nil)
609			tbuf=tbuf[0:len tbuf -1];
610		ixnew[t] = ftell(bt);
611		t++;
612	}
613}
614
615range(a, b : int, separator : string)
616{
617	if (a>b)
618		out.puts(sys->sprint("%d", b));
619	else
620		out.puts(sys->sprint("%d", a));
621	if (a < b)
622		out.puts(sys->sprint("%s%d", separator, b));
623}
624
625fetch(f : array of int, a,b : int , bp : ref Iobuf, s : string)
626{
627	buf : string;
628	bp.seek(big f[a-1], 0);
629	while (a++ <= b) {
630		buf=bp.gets('\n');
631		out.puts(s);
632		out.puts(buf);
633	}
634}
635
636change(a, b, c, d : int)
637{
638	if (a > b && c > d)
639		return;
640	anychange = 1;
641	if (mflag && firstchange == 0) {
642		out.puts(sys->sprint( "diff %s %s\n", file1, file2));
643		firstchange = 1;
644	}
645	if (mode != 'f') {
646		range(a, b, ",");
647		if (a>b)
648			out.putc('a');
649		else if (c>d)
650			out.putc('d');
651		else
652			out.putc('c');
653		if (mode != 'e')
654			range(c, d, ",");
655	}
656	else {
657		if (a>b)
658			out.putc('a');
659		else if (c>d)
660			out.putc('d');
661		else
662			out.putc('c');
663		range(a, b, " ");
664	}
665	out.putc('\n');
666	if (mode == 0) {
667		fetch(ixold, a, b, input[0], "< ");
668		if (a <= b && c <= d)
669			out.puts("---\n");
670	}
671	if (mode==0)
672		fetch(ixnew, c, d, input[1], "> ");
673	else
674		fetch(ixnew, c, d, input[1], "");
675
676	if (mode != 0 && c <= d)
677		out.puts(".\n");
678}
679
680
681######################### diffdir starts here ......
682
683scandir(name : string) : array of string
684{
685	(db,nitems):= readdir->init(name,Readdir->NAME);
686	cp := array[nitems] of string;
687	for(i:=0;i<nitems;i++)
688		cp[i]=db[i].name;
689	return cp;
690}
691
692
693diffdir(f, t : string, level : int)
694{
695	df, dt : array of string;
696	fb, tb : string;
697	i:=0;
698	j:=0;
699	df = scandir(f);
700	dt = scandir(t);
701	while ((i<len df) || (j<len dt)) {
702		if ((j==len dt) || (i<len df && df[i] < dt[j])) {
703			if (mode == 0)
704				out.puts(sys->sprint("Only in %s: %s\n", f, df[i]));
705			i++;
706			continue;
707		}
708		if ((i==len df) || (j<len dt && df[i] > dt[j])) {
709			if (mode == 0)
710				out.puts(sys->sprint("Only in %s: %s\n", t, dt[j]));
711			j++;
712			continue;
713		}
714		fb=sys->sprint("%s/%s", f, df[i]);
715		tb=sys->sprint("%s/%s", t, dt[j]);
716		diff(fb, tb, level+1);
717		i++; j++;
718	}
719}
720
721cmp(b0, b1: ref Iobuf): int
722{
723	b0.seek(big 0, Sys->SEEKSTART);
724	b1.seek(big 0, Sys->SEEKSTART);
725	buf0 := array[1024] of byte;
726	buf1 := array[1024] of byte;
727	for (;;) {
728		n0 := b0.read(buf0, len buf0);
729		n1 := b1.read(buf1, len buf1);
730
731		if (n0 != n1)
732			return 1;
733
734		if (n0 == 0)
735			return 0;
736
737		for (i := 0; i < n0; i++)
738			if (buf0[i] != buf1[i])
739				return 1;
740	}
741}
742
743################## main from here.....
744
745REGULAR_FILE(s : Sys->Dir) : int
746{
747	# both pipes and networks contain non-zero-length files
748	# which are not seekable.
749	return (s.qid.qtype&Sys->QTDIR) == 0 &&
750		s.dtype != '|' &&
751		s.dtype != 'I';
752#		&& s.length > 0;	device files have zero length.
753}
754
755rmtmpfiles()
756{
757	while (whichtmp > 0) {
758		whichtmp--;
759		sys->remove(tmpname[whichtmp]);
760	}
761}
762
763mktmpfile(inputf : ref Sys->FD) : (string, Sys->Dir)
764{
765	i, j : int;
766	sb : Sys->Dir;
767	p : string;
768	buf := array[8192] of byte;
769
770	p = tmpname[whichtmp++];
771	fd := sys->create(p, Sys->OWRITE, 8r600);
772	if (fd == nil) {
773		error(sys->sprint("cannot create %s: %r", p));
774		return (nil, sb);
775	}
776	while ((i = sys->read(inputf, buf, len buf)) > 0) {
777		if ((i = sys->write(fd, buf, i)) < 0)
778			break;
779	}
780	(j,sb)=sys->fstat(fd);
781	if (i < 0 || j < 0) {
782		error(sys->sprint("cannot read/write %s: %r", p));
783		return (nil, sb);
784	}
785	return (p, sb);
786}
787
788
789statfile(file : string) : (string,Sys->Dir)
790{
791	(ret,sb):=sys->stat(file);
792	if (ret==-1) {
793		if (file != "-" || sys->fstat(sys->fildes(0)).t0 == -1){
794			error(sys->sprint("cannot stat %s: %r", file));
795			return (nil,sb);
796		}
797		(file, sb) = mktmpfile(sys->fildes(0));
798	}
799	else if (!REGULAR_FILE(sb) && !(sb.qid.qtype&Sys->QTDIR)) {
800		if ((i := sys->open(file, Sys->OREAD)) == nil) {
801			error(sys->sprint("cannot open %s: %r", file));
802			return (nil, sb);
803		}
804		(file, sb) = mktmpfile(i);
805	}
806	return (file,sb);
807}
808
809diff(f, t : string, level : int)
810{
811	fp,tp,p,rest,fb,tb : string;
812	fsb, tsb : Sys->Dir;
813	(fp,fsb) = statfile(f);
814	if (fp == nil)
815		return;
816	(tp,tsb) = statfile(t);
817	if (tp == nil)
818		return;
819	if ((fsb.qid.qtype&Sys->QTDIR) && (tsb.qid.qtype&Sys->QTDIR)) {
820		if (rflag || level == 0)
821			diffdir(fp, tp, level);
822		else
823			out.puts(sys->sprint("Common subdirectories: %s and %s\n", fp, tp));
824	}
825	else if (REGULAR_FILE(fsb) && REGULAR_FILE(tsb)){
826		diffreg(fp, tp);
827	} else {
828		if (!(fsb.qid.qtype&Sys->QTDIR)) {
829			(p,rest)=str->splitr(f,"/");
830			if (rest!=nil)
831				p = rest;
832			tb=sys->sprint("%s/%s", tp, p);
833			diffreg(fp, tb);
834		}
835		else {
836			(p,rest)=str->splitr(t,"/");
837			if (rest!=nil)
838				p = rest;
839			fb=sys->sprint("%s/%s", fp, p);
840			diffreg(fb, tp);
841		}
842	}
843}
844
845fatal(s: string)
846{
847	sys->fprint(stderr, "diff: %s\n", s);
848	raise "fail:error";
849}
850
851error(s: string)
852{
853	sys->fprint(stderr, "diff: %s\n", s);
854}
855