implement Dbfs; # # Copyright © 1999 Vita Nuova Limited. All rights reserved. # Revisions copyright © 2002 Vita Nuova Holdings Limited. All rights reserved. # include "sys.m"; sys: Sys; Qid: import Sys; include "draw.m"; include "arg.m"; include "styx.m"; styx: Styx; Tmsg, Rmsg: import styx; include "styxservers.m"; styxservers: Styxservers; Fid, Styxserver, Navigator, Navop: import styxservers; Enotfound, Eperm, Ebadarg: import styxservers; include "bufio.m"; bufio: Bufio; Iobuf: import bufio; Record: adt { id: int; # file number in directory x: int; # index in file dirty: int; # modified but not written vers: int; # version data: array of byte; new: fn(x: array of byte): ref Record; print: fn(r: self ref Record, fd: ref Sys->FD); qid: fn(r: self ref Record): Sys->Qid; }; Database: adt { name: string; file: ref Iobuf; records: array of ref Record; dirty: int; vers: int; nextid: int; findrec: fn(db: self ref Database, id: int): ref Record; }; Dbfs: module { init: fn(nil: ref Draw->Context, nil: list of string); }; Qdir, Qnew, Qdata: con iota; clockfd: ref Sys->FD; stderr: ref Sys->FD; database: ref Database; user: string; Eremoved: con "file removed"; usage() { sys->fprint(stderr, "Usage: dbfs [-a|-b|-ac|-bc] [-D] file mountpoint\n"); raise "fail:usage"; } nomod(s: string) { sys->fprint(stderr, "dbfs: can't load %s: %r\n", s); raise "fail:load"; } init(nil: ref Draw->Context, args: list of string) { sys = load Sys Sys->PATH; sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil); stderr = sys->fildes(2); styx = load Styx Styx->PATH; if(styx == nil) nomod(Styx->PATH); styx->init(); styxservers = load Styxservers Styxservers->PATH; if(styxservers == nil) nomod(Styxservers->PATH); styxservers->init(styx); bufio = load Bufio Bufio->PATH; if(bufio == nil) nomod(Bufio->PATH); arg := load Arg Arg->PATH; if(arg == nil) nomod(Arg->PATH); arg->init(args); flags := Sys->MREPL; copt := 0; empty := 0; while((o := arg->opt()) != 0) case o { 'a' => flags = Sys->MAFTER; 'b' => flags = Sys->MBEFORE; 'c' => copt = 1; 'e' => empty = 1; 'D' => styxservers->traceset(1); * => usage(); } args = arg->argv(); arg = nil; if(len args != 2) usage(); if(copt) flags |= Sys->MCREATE; file := hd args; args = tl args; mountpt := hd args; df := bufio->open(file, Sys->OREAD); if(df == nil && empty){ (rc, nil) := sys->stat(file); if(rc < 0) df = bufio->create(file, Sys->OREAD, 8r600); } if(df == nil){ sys->fprint(stderr, "dbfs: can't open %s: %r\n", file); raise "fail:open"; } (db, err) := dbread(ref Database(file, df, nil, 0, 0, 0)); if(db == nil){ sys->fprint(stderr, "dbfs: can't read %s: %s\n", file, err); raise "fail:dbread"; } db.file = nil; # dbprint(db); database = db; sys->pctl(Sys->FORKFD, nil); user = rf("/dev/user"); if(user == nil) user = "inferno"; fds := array[2] of ref Sys->FD; if(sys->pipe(fds) < 0){ sys->fprint(stderr, "dbfs: can't create pipe: %r\n"); raise "fail:pipe"; } navops := chan of ref Navop; spawn navigator(navops); (tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qdir); fds[0] = nil; pidc := chan of int; spawn serveloop(tchan, srv, pidc, navops); <-pidc; if(sys->mount(fds[1], nil, mountpt, flags, nil) < 0) { sys->fprint(stderr, "dbfs: mount failed: %r\n"); raise "fail:mount"; } } rf(f: string): string { fd := sys->open(f, Sys->OREAD); if(fd == nil) return nil; b := array[Sys->NAMEMAX] of byte; n := sys->read(fd, b, len b); if(n < 0) return nil; return string b[0:n]; } dbread(db: ref Database): (ref Database, string) { db.file.seek(big 0, Sys->SEEKSTART); rl: list of ref Record; n := 0; for(;;){ (r, err) := getrec(db); if(err != nil) return (nil, err); # could press on without it, or make it the `file' contents if(r == nil) break; rl = r :: rl; n++; } db.nextid = n; db.records = array[n] of ref Record; for(; rl != nil; rl = tl rl){ r := hd rl; n--; r.id = n; r.x = n; db.records[n] = r; } return (db, nil); } # # a record is (.+\n)*\n # getrec(db: ref Database): (ref Record, string) { r := ref Record(-1, -1, 0, 0, nil); data := ""; for(;;){ s := db.file.gets('\n'); if(s == nil){ if(data == nil) return (nil, nil); # BUG: distinguish i/o error from EOF? break; } if(s[len s - 1] != '\n') # return (nil, "file missing newline"); # possibly truncated s += "\n"; if(s == "\n") break; data += s; } r.data = array of byte data; return (r, nil); } dbsync(db: ref Database): int { if(db.dirty){ db.file = bufio->create(db.name, Sys->OWRITE, 8r666); if(db.file == nil) return -1; for(i := 0; i < len db.records; i++){ r := db.records[i]; if(r != nil && r.data != nil){ if(db.file.write(r.data, len r.data) != len r.data) return -1; db.file.putc('\n'); } } if(db.file.flush()) return -1; db.file = nil; db.dirty = 0; } return 0; } dbprint(db: ref Database) { stdout := sys->fildes(1); for(i := 0; i < len db.records; i++){ db.records[i].print(stdout); sys->print("\n"); } } Database.findrec(db: self ref Database, id: int): ref Record { for(i:=0; iFD) { if(r.data != nil) sys->write(fd, r.data, len r.data); } Record.qid(r: self ref Record): Sys->Qid { return Sys->Qid(QPATH(r.x, Qdata), r.vers, Sys->QTFILE); } serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop) { pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::nil); Serve: while((gm := <-tchan) != nil){ pick m := gm { Readerror => sys->fprint(stderr, "dbfs: fatal read error: %s\n", m.error); break Serve; Open => c := srv.getfid(m.fid); if(c == nil || TYPE(c.path) != Qnew){ srv.open(m); # default action break; } if(c.uname != user) { srv.reply(ref Rmsg.Error(m.tag, Eperm)); break; } mode := styxservers->openmode(m.mode); if(mode < 0) { srv.reply(ref Rmsg.Error(m.tag, Ebadarg)); break; } # generate new file, change Fid's qid to match r := Record.new(array[0] of byte); qid := r.qid(); c.open(mode, qid); srv.reply(ref Rmsg.Open(m.tag, qid, srv.iounit())); Read => (c, err) := srv.canread(m); if(c == nil){ srv.reply(ref Rmsg.Error(m.tag, err)); break; } if(c.qtype & Sys->QTDIR){ srv.read(m); # does readdir break; } r := database.records[FILENO(c.path)]; if(r == nil) srv.reply(ref Rmsg.Error(m.tag, Eremoved)); else srv.reply(styxservers->readbytes(m, r.data)); Write => (c, merr) := srv.canwrite(m); if(c == nil){ srv.reply(ref Rmsg.Error(m.tag, merr)); break; } (value, err) := data2rec(m.data); if(err != nil){ srv.reply(ref Rmsg.Error(m.tag, err)); break; } fno := FILENO(c.path); r := database.records[fno]; if(r == nil){ srv.reply(ref Rmsg.Error(m.tag, Eremoved)); break; } r.data = value; r.vers++; database.dirty++; if(dbsync(database) == 0) srv.reply(ref Rmsg.Write(m.tag, len m.data)); else srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r"))); Clunk => # a transaction-oriented dbfs could delay updating the record until clunk srv.clunk(m); Remove => c := srv.getfid(m.fid); if(c == nil || c.qtype & Sys->QTDIR || TYPE(c.path) != Qdata){ # let it diagnose all the errors srv.remove(m); break; } r := database.records[FILENO(c.path)]; if(r != nil) r.data = nil; database.dirty++; srv.delfid(c); if(dbsync(database) == 0) srv.reply(ref Rmsg.Remove(m.tag)); else srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r"))); Wstat => srv.default(gm); # TO DO? * => srv.default(gm); } } navops <-= nil; # shut down navigator } dirslot(n: int): int { for(i := 0; i < len database.records; i++){ r := database.records[i]; if(r != nil && r.data != nil){ if(n == 0) return i; n--; } } return -1; } dir(qid: Sys->Qid, name: string, length: big, uid: string, perm: int): ref Sys->Dir { d := ref sys->zerodir; d.qid = qid; if(qid.qtype & Sys->QTDIR) perm |= Sys->DMDIR; d.mode = perm; d.name = name; d.uid = uid; d.gid = uid; d.length = length; return d; } dirgen(p: big): (ref Sys->Dir, string) { case TYPE(p) { Qdir => return (dir(Qid(QPATH(0, Qdir),database.vers,Sys->QTDIR), "/", big 0, user, 8r700), nil); Qnew => return (dir(Qid(QPATH(0, Qnew),0,Sys->QTFILE), "new", big 0, user, 8r600), nil); * => n := FILENO(p); if(n < 0 || n >= len database.records) return (nil, nil); r := database.records[n]; if(r == nil || r.data == nil) return (nil, Enotfound); return (dir(r.qid(), sys->sprint("%d", r.id), big len r.data, user, 8r600), nil); } } navigator(navops: chan of ref Navop) { while((m := <-navops) != nil){ pick n := m { Stat => n.reply <-= dirgen(n.path); Walk => if(int n.path != Qdir){ n.reply <-= (nil, "not a directory"); break; } case n.name { ".." => ; # nop "new" => n.path = QPATH(0, Qnew); * => if(len n.name < 1 || !(n.name[0]>='0' && n.name[0]<='9')){ # weak test for now n.reply <-= (nil, Enotfound); continue; } r := database.findrec(int n.name); if(r == nil){ n.reply <-= (nil, Enotfound); continue; } n.path = QPATH(r.x, Qdata); } n.reply <-= dirgen(n.path); Readdir => if(int m.path != Qdir){ n.reply <-= (nil, "not a directory"); break; } i := n.offset; if(i == 0) n.reply <-= dirgen(QPATH(0,Qnew)); for(; --n.count >= 0 && (j := dirslot(i)) >= 0; i++) n.reply <-= dirgen(QPATH(j,Qdata)); # n² but the file will be small n.reply <-= (nil, nil); } } } QPATH(w, q: int): big { return big ((w<<8)|q); } TYPE(path: big): int { return int path & 16rFF; } FILENO(path: big) : int { return (int path >> 8) & 16rFFFFFF; } # # a record is (.+\n)*, without final empty line # data2rec(data: array of byte): (array of byte, string) { s: string; for(b := data; len b > 0;){ (b, s) = getline(b); if(s == nil || s[len s - 1] != '\n' || s == "\n") return (nil, "partial or malformed record"); # possibly truncated } return (data, nil); } getline(b: array of byte): (array of byte, string) { n := len b; for(i := 0; i < n; i++){ (ch, l, nil) := sys->byte2char(b, i); i += l; if(l == 0 || ch == '\n') break; } return (b[i:], string b[0:i]); }