xref: /inferno-os/appl/cmd/dbfs.b (revision 3f1f06c5d12b24c4061e5123acabf72348ff45a2)
1implement Dbfs;
2
3#
4# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
5# Revisions copyright © 2002 Vita Nuova Holdings Limited.  All rights reserved.
6#
7
8include "sys.m";
9	sys: Sys;
10	Qid: import Sys;
11
12include "draw.m";
13
14include "arg.m";
15
16include "styx.m";
17	styx: Styx;
18	Tmsg, Rmsg: import styx;
19
20include "styxservers.m";
21	styxservers: Styxservers;
22	Fid, Styxserver, Navigator, Navop: import styxservers;
23	Enotfound, Eperm, Ebadarg: import styxservers;
24
25include "bufio.m";
26	bufio: Bufio;
27	Iobuf: import bufio;
28
29Record: adt {
30	id:		int;		# file number in directory
31	x:		int;		# index in file
32	dirty:	int;		# modified but not written
33	vers:		int;		# version
34	data:		array of byte;
35
36	new:		fn(x: array of byte): ref Record;
37	print:	fn(r: self ref Record, fd: ref Sys->FD);
38	qid:		fn(r: self ref Record): Sys->Qid;
39};
40
41Database: adt {
42	name:	string;
43	file:	ref Iobuf;
44	records:	array of ref Record;
45	dirty:	int;
46	vers:		int;
47	nextid:	int;
48
49	findrec:	fn(db: self ref Database, id: int): ref Record;
50};
51
52Dbfs: module
53{
54	init: fn(nil: ref Draw->Context, nil: list of string);
55};
56
57Qdir, Qnew, Qdata: con iota;
58
59clockfd: ref Sys->FD;
60stderr: ref Sys->FD;
61database: ref Database;
62user: string;
63Eremoved: con "file removed";
64
65usage()
66{
67	sys->fprint(stderr, "Usage: dbfs [-a|-b|-ac|-bc] [-D] file mountpoint\n");
68	raise "fail:usage";
69}
70
71nomod(s: string)
72{
73	sys->fprint(stderr, "dbfs: can't load %s: %r\n", s);
74	raise "fail:load";
75}
76
77init(nil: ref Draw->Context, args: list of string)
78{
79	sys = load Sys Sys->PATH;
80	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
81	stderr = sys->fildes(2);
82	styx = load Styx Styx->PATH;
83	if(styx == nil)
84		nomod(Styx->PATH);
85	styx->init();
86	styxservers = load Styxservers Styxservers->PATH;
87	if(styxservers == nil)
88		nomod(Styxservers->PATH);
89	styxservers->init(styx);
90	bufio = load Bufio Bufio->PATH;
91	if(bufio == nil)
92		nomod(Bufio->PATH);
93
94	arg := load Arg Arg->PATH;
95	if(arg == nil)
96		nomod(Arg->PATH);
97	arg->init(args);
98	flags := Sys->MREPL;
99	copt := 0;
100	empty := 0;
101	while((o := arg->opt()) != 0)
102		case o {
103		'a' =>	flags = Sys->MAFTER;
104		'b' =>	flags = Sys->MBEFORE;
105		'c' =>	copt = 1;
106		'e' =>	empty = 1;
107		'D' =>	styxservers->traceset(1);
108		* =>		usage();
109		}
110	args = arg->argv();
111	arg = nil;
112
113	if(len args != 2)
114		usage();
115	if(copt)
116		flags |= Sys->MCREATE;
117	file := hd args;
118	args = tl args;
119	mountpt := hd args;
120
121	df := bufio->open(file, Sys->OREAD);
122	if(df == nil && empty){
123		(rc, nil) := sys->stat(file);
124		if(rc < 0)
125			df = bufio->create(file, Sys->OREAD, 8r600);
126	}
127	if(df == nil){
128		sys->fprint(stderr, "dbfs: can't open %s: %r\n", file);
129		raise "fail:open";
130	}
131	(db, err) := dbread(ref Database(file, df, nil, 0, 0, 0));
132	if(db == nil){
133		sys->fprint(stderr, "dbfs: can't read %s: %s\n", file, err);
134		raise "fail:dbread";
135	}
136	db.file = nil;
137#	dbprint(db);
138	database = db;
139
140	sys->pctl(Sys->FORKFD, nil);
141
142	user = rf("/dev/user");
143	if(user == nil)
144		user = "inferno";
145
146	fds := array[2] of ref Sys->FD;
147	if(sys->pipe(fds) < 0){
148		sys->fprint(stderr, "dbfs: can't create pipe: %r\n");
149		raise "fail:pipe";
150	}
151
152	navops := chan of ref Navop;
153	spawn navigator(navops);
154
155	(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qdir);
156	fds[0] = nil;
157
158	pidc := chan of int;
159	spawn serveloop(tchan, srv, pidc, navops);
160	<-pidc;
161
162	if(sys->mount(fds[1], nil, mountpt, flags, nil) < 0) {
163		sys->fprint(stderr, "dbfs: mount failed: %r\n");
164		raise "fail:mount";
165	}
166}
167
168rf(f: string): string
169{
170	fd := sys->open(f, Sys->OREAD);
171	if(fd == nil)
172		return nil;
173	b := array[Sys->NAMEMAX] of byte;
174	n := sys->read(fd, b, len b);
175	if(n < 0)
176		return nil;
177	return string b[0:n];
178}
179
180dbread(db: ref Database): (ref Database, string)
181{
182	db.file.seek(big 0, Sys->SEEKSTART);
183	rl: list of ref Record;
184	n := 0;
185	for(;;){
186		(r, err) := getrec(db);
187		if(err != nil)
188			return (nil, err);		# could press on without it, or make it the `file' contents
189		if(r == nil)
190			break;
191		rl = r :: rl;
192		n++;
193	}
194	db.nextid = n;
195	db.records = array[n] of ref Record;
196	for(; rl != nil; rl = tl rl){
197		r := hd rl;
198		n--;
199		r.id = n;
200		r.x = n;
201		db.records[n] = r;
202	}
203	return (db, nil);
204}
205
206#
207# a record is (.+\n)*\n
208#
209getrec(db: ref Database): (ref Record, string)
210{
211	r := ref Record(-1, -1, 0, 0, nil);
212	data := "";
213	for(;;){
214		s := db.file.gets('\n');
215		if(s == nil){
216			if(data == nil)
217				return (nil, nil);		# BUG: distinguish i/o error from EOF?
218			break;
219		}
220		if(s[len s - 1] != '\n')
221#			return (nil, "file missing newline");	# possibly truncated
222			s += "\n";
223		if(s == "\n")
224			break;
225		data += s;
226	}
227	r.data = array of byte data;
228	return (r, nil);
229}
230
231dbsync(db: ref Database): int
232{
233	if(db.dirty){
234		db.file = bufio->create(db.name, Sys->OWRITE, 8r666);
235		if(db.file == nil)
236			return -1;
237		for(i := 0; i < len db.records; i++){
238			r := db.records[i];
239			if(r != nil && r.data != nil){
240				if(db.file.write(r.data, len r.data) != len r.data)
241					return -1;
242				db.file.putc('\n');
243			}
244		}
245		if(db.file.flush())
246			return -1;
247		db.file = nil;
248		db.dirty = 0;
249	}
250	return 0;
251}
252
253dbprint(db: ref Database)
254{
255	stdout := sys->fildes(1);
256	for(i := 0; i < len db.records; i++){
257		db.records[i].print(stdout);
258		sys->print("\n");
259	}
260}
261
262Database.findrec(db: self ref Database, id: int): ref Record
263{
264	for(i:=0; i<len db.records; i++)
265		if((r := db.records[i]) != nil && r.id == id)
266			return r;
267	return nil;
268}
269
270Record.new(fields: array of byte): ref Record
271{
272	n := len database.records;
273	r := ref Record(n, n, 0, 0, fields);
274	a := array[n+1] of ref Record;
275	if(n)
276		a[0:] = database.records[0:];
277	a[n] = r;
278	database.records = a;
279	database.vers++;
280	return r;
281}
282
283Record.print(r: self ref Record, fd: ref Sys->FD)
284{
285	if(r.data != nil)
286		sys->write(fd, r.data, len r.data);
287}
288
289Record.qid(r: self ref Record): Sys->Qid
290{
291	return Sys->Qid(QPATH(r.x, Qdata), r.vers, Sys->QTFILE);
292}
293
294serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop)
295{
296	pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::nil);
297Serve:
298	while((gm := <-tchan) != nil){
299		pick m := gm {
300		Readerror =>
301			sys->fprint(stderr, "dbfs: fatal read error: %s\n", m.error);
302			break Serve;
303		Open =>
304			c := srv.getfid(m.fid);
305			if(c == nil || TYPE(c.path) != Qnew){
306				srv.open(m);		# default action
307				break;
308			}
309			if(c.uname != user) {
310				srv.reply(ref Rmsg.Error(m.tag, Eperm));
311				break;
312			}
313			mode := styxservers->openmode(m.mode);
314			if(mode < 0) {
315				srv.reply(ref Rmsg.Error(m.tag, Ebadarg));
316				break;
317			}
318			# generate new file, change Fid's qid to match
319			r := Record.new(array[0] of byte);
320			qid := r.qid();
321			c.open(mode, qid);
322			srv.reply(ref Rmsg.Open(m.tag, qid, srv.iounit()));
323		Read =>
324			(c, err) := srv.canread(m);
325			if(c == nil){
326				srv.reply(ref Rmsg.Error(m.tag, err));
327				break;
328			}
329			if(c.qtype & Sys->QTDIR){
330				srv.read(m);	# does readdir
331				break;
332			}
333			r := database.records[FILENO(c.path)];
334			if(r == nil)
335				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
336			else
337				srv.reply(styxservers->readbytes(m, r.data));
338		Write =>
339			(c, merr) := srv.canwrite(m);
340			if(c == nil){
341				srv.reply(ref Rmsg.Error(m.tag, merr));
342				break;
343			}
344			(value, err) := data2rec(m.data);
345			if(err != nil){
346				srv.reply(ref Rmsg.Error(m.tag, err));
347				break;
348			}
349			fno := FILENO(c.path);
350			r := database.records[fno];
351			if(r == nil){
352				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
353				break;
354			}
355			r.data = value;
356			r.vers++;
357			database.dirty++;
358			if(dbsync(database) == 0)
359				srv.reply(ref Rmsg.Write(m.tag, len m.data));
360			else
361				srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
362		Clunk =>
363			# a transaction-oriented dbfs could delay updating the record until clunk
364			srv.clunk(m);
365		Remove =>
366			c := srv.getfid(m.fid);
367			if(c == nil || c.qtype & Sys->QTDIR || TYPE(c.path) != Qdata){
368				# let it diagnose all the errors
369				srv.remove(m);
370				break;
371			}
372			r := database.records[FILENO(c.path)];
373			if(r != nil)
374				r.data = nil;
375			database.dirty++;
376			srv.delfid(c);
377			if(dbsync(database) == 0)
378				srv.reply(ref Rmsg.Remove(m.tag));
379			else
380				srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
381		Wstat =>
382			srv.default(gm);	# TO DO?
383		* =>
384			srv.default(gm);
385		}
386	}
387	navops <-= nil;		# shut down navigator
388}
389
390dirslot(n: int): int
391{
392	for(i := 0; i < len database.records; i++){
393		r := database.records[i];
394		if(r != nil && r.data != nil){
395			if(n == 0)
396				return i;
397			n--;
398		}
399	}
400	return -1;
401}
402
403dir(qid: Sys->Qid, name: string, length: big, uid: string, perm: int): ref Sys->Dir
404{
405	d := ref sys->zerodir;
406	d.qid = qid;
407	if(qid.qtype & Sys->QTDIR)
408		perm |= Sys->DMDIR;
409	d.mode = perm;
410	d.name = name;
411	d.uid = uid;
412	d.gid = uid;
413	d.length = length;
414	return d;
415}
416
417dirgen(p: big): (ref Sys->Dir, string)
418{
419	case TYPE(p) {
420	Qdir =>
421		return (dir(Qid(QPATH(0, Qdir),database.vers,Sys->QTDIR), "/", big 0, user, 8r700), nil);
422	Qnew =>
423		return (dir(Qid(QPATH(0, Qnew),0,Sys->QTFILE), "new", big 0, user, 8r600), nil);
424	* =>
425		n := FILENO(p);
426		if(n < 0 || n >= len database.records)
427			return (nil, nil);
428		r := database.records[n];
429		if(r == nil || r.data == nil)
430			return (nil, Enotfound);
431		return (dir(r.qid(), sys->sprint("%d", r.id), big len r.data, user, 8r600), nil);
432	}
433}
434
435navigator(navops: chan of ref Navop)
436{
437	while((m := <-navops) != nil){
438		pick n := m {
439		Stat =>
440			n.reply <-= dirgen(n.path);
441		Walk =>
442			if(int n.path != Qdir){
443				n.reply <-= (nil, "not a directory");
444				break;
445			}
446			case n.name {
447			".." =>
448				;	# nop
449			"new" =>
450				n.path = QPATH(0, Qnew);
451			* =>
452				if(len n.name < 1 || !(n.name[0]>='0' && n.name[0]<='9')){	# weak test for now
453					n.reply <-= (nil, Enotfound);
454					continue;
455				}
456				r := database.findrec(int n.name);
457				if(r == nil){
458					n.reply <-= (nil, Enotfound);
459					continue;
460				}
461				n.path = QPATH(r.x, Qdata);
462			}
463			n.reply <-= dirgen(n.path);
464		Readdir =>
465			if(int m.path != Qdir){
466				n.reply <-= (nil, "not a directory");
467				break;
468			}
469			i := n.offset;
470			if(i == 0)
471				n.reply <-= dirgen(QPATH(0,Qnew));
472			for(; --n.count >= 0 && (j := dirslot(i)) >= 0; i++)
473				n.reply <-= dirgen(QPATH(j,Qdata));	# n² but the file will be small
474			n.reply <-= (nil, nil);
475		}
476	}
477}
478
479QPATH(w, q: int): big
480{
481	return big ((w<<8)|q);
482}
483
484TYPE(path: big): int
485{
486	return int path & 16rFF;
487}
488
489FILENO(path: big) : int
490{
491	return (int path >> 8) & 16rFFFFFF;
492}
493
494#
495# a record is (.+\n)*, without final empty line
496#
497data2rec(data: array of byte): (array of byte, string)
498{
499	s: string;
500	for(b := data; len b > 0;){
501		(b, s) = getline(b);
502		if(s == nil || s[len s - 1] != '\n' || s == "\n")
503			return (nil, "partial or malformed record");	# possibly truncated
504	}
505	return (data, nil);
506}
507
508getline(b: array of byte): (array of byte, string)
509{
510	n := len b;
511	for(i := 0; i < n; i++){
512		(ch, l, nil) := sys->byte2char(b, i);
513		i += l;
514		if(l == 0 || ch == '\n')
515			break;
516	}
517	return (b[i:], string b[0:i]);
518}
519