add games/wadfs

This commit is contained in:
qwx 2017-08-10 11:39:18 +02:00
parent 4be612946f
commit 16ef6e5596
3 changed files with 1054 additions and 0 deletions

192
sys/man/4/wadfs Normal file
View file

@ -0,0 +1,192 @@
.TH WADFS 4
.SH NAME
wadfs \- WAD file system
.SH SYNOPSIS
.B wadfs
[
.B -Dr
] [
.B -m
.I mtpt
] [
.B -S
.I srvname
] [
.I WAD
]
.SH DESCRIPTION
.I Wadfs
serves a file tree mounted at
.I mtpt
(default
.BR /mnt/wad )
that provides access to a
.I WAD
file's contents.
.PP
The command line options are:
.TF "-S srvname"
.TP
.B -D
Enable 9P debugging messages.
.TP
.B -r
Set read-only file tree.
.TP
.BI -S \ srvname
Post channel on
.RI /srv/ srvname .
.TP
.BI -m \ mtpt
Set mountpoint.
.PD
.PP
A
.I WAD
is a concatenation of uncompressed files, referred to as lumps.
A lump may contain either data,
or be used as a marker to indicate the beginning or end of a section,
segregating lumps of the same format.
.PP
.I Wadfs
represents section start markers as directories,
and regular lumps and end markers as files.
For convenience, lump file names are in lower case,
and are translated to the upper case internally.
.PP
At startup, if the path to a
.I WAD
file is provided as argument,
.I wadfs
will attempt to parse it and construct a file tree.
Otherwise,
.I wadfs
starts with a blank tree instead.
.PP
Two additional files are provided in the file system's root directory:
.L SIG
and
.LR WAD .
Reading from and writing to
.L SIG
allows accessing and changing the
.IR WAD 's
type.
The only possible values are
.L PWAD
(the default) and
.LR IWAD .
.PP
.L WAD
returns the new
.I WAD
file resulting from the recompilation of the lump tree.
.SS "WAD file structure"
There are few restrictions on the structure of
.I WAD
files.
Excepting maps, sections can nest and may have no end marker,
or one named differently than the section itself.
Regular sections typically have one-letter names,
and nested sections use the same name appended by a digit.
By convention,
lump names may only contain visible printing
.SM ASCII
characters,
excepting lower-case letters.
Map sections do not end at a marker but at the next non map lump,
and use hardcoded names, depending on game version.
.PP
.I Wadfs
imposes a number of additional restrictions on structure and naming:
.IP • 3
Lump names may not contain upper-case letters and the
.L /
character.
.IP •
A map section may only contain map lumps, which use hardcoded names.
Ordering is significant, but is handled automatically.
Map sections may not nest.
.IP •
Regular sections may not nest beyond one level,
and may not contain more than one end marker.
End markers may not exist outside of a section.
Directory names omit the start marker's
.L "_START"
suffix.
.IP •
Excepting map lumps, no two lumps, including markers,
may have the same name.
.IP •
Once created, a lump may not be renamed so as to change its type.
.SS "Error recovery"
Upon parsing the initial
.I WAD
file, if one of the restrictions for
.I WAD
file structure outlined in the sections above is not respected,
a warning is issued, and the offending lump is potentially skipped.
Some recovery is attempted,
but one must systematically recheck the tree.
When duplicate non marker lumps are encountered,
each will overwrite the previous entry.
.SH EXAMPLES
Open
.B doom2.wad
and play a MUS file:
.IP
.EX
% wadfs /sys/games/lib/doom/doom2.wad
createfile SW18_7: file already exists
% games/mus /mnt/wad/d_romero | games/midi
.EE
.PP
Now create a blank
.IR WAD ,
then one section
.LR FF ;
copy a flat from
.B doom2.wad
to the directory,
then rename the end marker to
.L F_END
to have the
.B doom
engine find the flat;
finally, compile and save the new
.I WAD
file.
.IP
.EX
% wadfs -m /mnt/wad2
% cd /mnt/wad2
% mkdir ff
adding end marker FF_END
% cp ../wad/f/f1/f_sky1 ff/
% mv ff/ff_end ff/f_end
% cp WAD /sys/games/lib/doom/sky.wad
.EE
.SH SOURCE
.B /sys/src/games/wadfs.c
.SH "SEE ALSO"
.IR games (1),
.IR mus (1)
.SH HISTORY
.I Wadfs
first appeared in 9front (August, 2017).
.SH BUGS
Many
.I WAD
files in the wild do not conform to all the rules exposed above,
in particular ones using
.SM DeHackEd
engine modifications.
.IR WAD 's
using end markers outside of a section,
typically
.LR F_END ,
will lose them.
.PP
Repairing broken
.I WAD
files can be a pain.

View file

@ -15,6 +15,7 @@ TARG=4s\
packet\
mandel\
midi\
wadfs\
OFILES=
HFILES=

861
sys/src/games/wadfs.c Normal file
View file

@ -0,0 +1,861 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <bio.h>
enum{
Nsig = 4,
Nhdr = Nsig+4+4,
Ndict = 4+4+8,
Nname = 8,
Nbuf = 8192,
Maxsz = 0x7fffffff - Nhdr
};
enum{
LTnil,
LTreg,
LTmap,
LTmrk,
LTend
};
typedef struct Lump Lump;
struct Lump{
char name[Nname+1];
u32int ofs;
uchar *buf;
ulong nbuf;
int type;
File *f;
Lump *l;
Lump *lp;
};
Lump l1 = {.l = &l1, .lp = &l1}, *lumps = &l1;
Biobuf *wad;
u32int nlmp;
File *ldir, *fsig, *fwad;
int rdonly, dirty;
Srv fs;
char *mapn[] = {
"things", "linedefs", "sidedefs", "vertexes", "segs",
"ssectors", "nodes", "sectors", "reject", "blockmap"
};
void
strupr(char *s, char *p)
{
char c;
do{
c = *p++;
*s++ = toupper(c);
}while(c != 0);
}
void
strlwr(char *s, char *p)
{
char c;
do{
c = *p++;
*s++ = tolower(c);
}while(c != 0);
}
void
link(Lump *l, Lump *lp, int len)
{
l->lp = lp;
l->l = lp->l;
lp->l->lp = l;
lp->l = l;
nlmp++;
fwad->length += Ndict + len;
}
void
unlink(Lump *l)
{
if(l->l == nil)
return;
l->lp->l = l->l;
l->l->lp = l->lp;
l->l = nil;
nlmp--;
fwad->length -= Ndict + (l->f != nil ? l->f->length : 0);
}
void
freelump(Lump *l)
{
unlink(l);
free(l->buf);
free(l);
}
void
readlump(Lump *l, uchar *p, long n)
{
if(n <= 0)
return;
Bseek(wad, l->ofs, 0);
if(Bread(wad, p, n) != n)
fprint(2, "readlump: short read: %r\n");
}
void
loadlump(File *f, ulong n)
{
Lump *l;
l = f->aux;
if(f->length > n)
n = f->length;
l->buf = emalloc9p(n);
l->nbuf = n;
l->ofs = 0;
readlump(l, l->buf, f->length);
}
Lump *
lastlump(Lump *lp)
{
File *f, *dir;
for(dir=lp->f, f=lp->l->f; lp->l!=lumps; lp=lp->l, f=lp->l->f)
if(f->parent != dir && f->parent->parent != dir)
break;
if(lp->type == LTend && lp->f->parent == dir)
lp = lp->lp;
return lp;
}
int
nextmaplump(char *s)
{
char **p;
for(p=mapn; p<mapn+nelem(mapn); p++)
if(strcmp(s, *p) == 0)
return p-mapn;
return -1;
}
Lump *
sortmap(Lump *lp, Lump *l)
{
int ip, i;
i = nextmaplump(l->f->name);
for(; lp->l != lumps; lp=lp->l){
ip = nextmaplump(lp->l->f->name);
if(ip < 0 || ip > i)
break;
}
return lp;
}
int
ismaplump(char *s)
{
return nextmaplump(s) >= 0;
}
int
ismapname(char *s)
{
if(strncmp(s, "map", 3) == 0)
return isdigit(s[3]) && isdigit(s[4]);
return s[0] == 'e' && isdigit(s[1])
&& s[2] == 'm' && isdigit(s[3]);
}
int
ismarkname(char *s, char *m)
{
char *p;
p = strstr(s, m);
if(p == nil || p[strlen(m)] != 0)
return 0;
if(p - s > 2)
return 0;
return 1;
}
int
validname(char *s, File *dir, int *type, int isnew, int isdir)
{
int n;
char c, *p;
Lump *lp;
*type = LTnil;
n = strlen(s);
if(n < 1 || n > sizeof(lp->name)-1){
werrstr("invalid lump name");
return 0;
}
for(p=s+n-1; c=*p, p-->=s;)
if(!isprint(c) || isupper(c) || c == '/'){
werrstr("invalid char %c in filename", c);
return 0;
}
if(isnew && !ismaplump(s))
for(lp=lumps->l; lp!=lumps; lp=lp->l)
if(cistrcmp(s, lp->name) == 0){
werrstr("duplicate non map lump");
return 0;
}
*type = LTreg;
lp = dir->aux;
if(ismapname(s)){
*type = LTmap;
if(isnew && !isdir){
werrstr("map marker not a directory");
return 0;
}else if(dir != fs.tree->root){
werrstr("nested map directory");
return 0;
}
return 1;
}else if(ismarkname(s, "_end")){
*type = LTend;
if(dir == fs.tree->root || lp == nil || lp->type == LTmap){
werrstr("orphaned end marker");
return 0;
}
return 1;
}else if(ismarkname(s, "_start")){
*type = LTmrk;
if(isnew){
werrstr("not allowed");
return 0;
}
goto mrkchk;
}else if(isnew && isdir){
*type = LTmrk;
if(n > 2){
werrstr("marker name too long");
return 0;
}
mrkchk:
if(dir->parent != fs.tree->root){
werrstr("start marker nested too deep");
return 0;
}else if(lp != nil && lp->type == LTmap){
werrstr("start marker within map directory");
return 0;
}
return 1;
}else if(ismaplump(s) ^ (lp != nil && lp->type == LTmap)){
werrstr("map lump outside of map directory");
return 0;
}
return 1;
}
int
endldir(Lump *lp, Lump *le)
{
char *s, name[sizeof lp->name];
Lump *l;
File *f;
l = emalloc9p(sizeof *l);
strcpy(l->name, lp->name);
s = strrchr(l->name, '_');
strcpy(s, "_END");
strlwr(name, l->name);
fprint(2, "adding end marker %s\n", l->name);
if(!validname(name, lp->f, &l->type, 1, 0) || l->type != LTend)
goto err;
f = createfile(lp->f, name, nil, lp->f->mode & 0666, l);
if(f == nil)
goto err;
closefile(f);
l->f = f;
link(l, le, 0);
return 0;
err:
free(l);
return -1;
}
void
accessfile(File *f, int mode)
{
f->atime = time(nil);
if(mode & AWRITE){
f->mtime = f->atime;
f->qid.vers++;
dirty = 1;
}
}
void
fswstat(Req *r)
{
int type;
char *e;
File *f, *fp;
Lump *lp;
e = "permission denied";
if(rdonly)
goto err;
if(r->d.mode != ~0 || r->d.gid[0] != 0)
goto err;
f = r->fid->file;
lp = f->aux;
if(r->d.length != ~0 && r->d.length != f->length){
if(f == fsig || f->mode & DMDIR)
goto err;
if(!hasperm(f, r->fid->uid, AWRITE))
goto err;
if(r->d.length < 0){
e = "invalid file length";
goto err;
}
if(fwad->length - f->length + r->d.length >= Maxsz){
e = "lump size exceeds wad limit";
goto err;
}
}
if(r->d.name[0] != 0 && strcmp(r->d.name, f->name) != 0){
fp = f->parent;
if(fp == nil){
e = "orphaned file";
goto err;
}
if(!hasperm(fp, r->fid->uid, AWRITE))
goto err;
if(!validname(r->d.name, fp, &type, 1, f->mode & DMDIR)){
responderror(r);
return;
}
if(lp->type != type){
e = "incompatible lump type";
goto err;
}
incref(fp);
fp = walkfile(fp, r->d.name);
if(fp != nil){
e = "file already exists";
goto err;
}
}
if(r->d.length != ~0 && r->d.length != f->length){
if(lp->buf == nil)
loadlump(f, r->d.length);
fwad->length += r->d.length - f->length;
f->length = r->d.length;
}
if(r->d.name[0] != 0 && strcmp(r->d.name, f->name) != 0){
free(f->name);
f->name = estrdup9p(r->d.name);
strupr(lp->name, f->name);
if(lp->type == LTmrk)
strcat(lp->name, "_START");
}
accessfile(f, AWRITE);
if(r->d.mtime != ~0)
f->mtime = r->d.mtime;
respond(r, nil);
return;
err:
respond(r, e);
}
void
fsremove(Req *r)
{
File *f;
Lump *lp;
f = r->fid->file;
lp = f->aux;
if(f == fsig || f == fwad){
respond(r, "not allowed");
return;
}else if(lp->l->f != nil && lp->l->f->parent == f){
respond(r, "has children");
return;
}
unlink(f->aux);
dirty = 1;
respond(r, nil);
}
char *
writesig(uchar *buf, char *s, vlong n)
{
if(n > Nsig+1 || strncmp(s, "IWAD", Nsig) != 0 && strncmp(s, "PWAD", Nsig) != 0)
return "invalid wad signature";
memcpy(buf, s, Nsig);
dirty = 1;
return nil;
}
void
fswrite(Req *r)
{
vlong n, m, ofs, end;
File *f;
Lump *l;
f = r->fid->file;
n = r->ifcall.count;
ofs = r->ifcall.offset;
if(f->mode & DMAPPEND)
ofs = f->length;
end = ofs + n;
l = f->aux;
if(f == fsig){
respond(r, writesig(l->buf, r->ifcall.data, n));
return;
}
if(l->buf == nil)
loadlump(f, end + Nbuf);
if(end > l->nbuf){
m = l->nbuf + Nbuf > end ? l->nbuf + Nbuf : end;
if(fwad->length - l->nbuf + m >= Maxsz){
respond(r, "lump size exceeds wad limit");
return;
}
l->buf = erealloc9p(l->buf, m);
l->nbuf = m;
}
memcpy(l->buf + ofs, r->ifcall.data, n);
m = end - f->length;
if(m > 0){
f->length += m;
fwad->length += m;
}
accessfile(f, AWRITE);
r->ofcall.count = n;
respond(r, nil);
}
void
makewad(void)
{
vlong n;
uchar *p;
u32int ofs;
Lump *l, *lp;
l = fwad->aux;
free(l->buf);
l->buf = emalloc9p(fwad->length);
p = l->buf;
lp = fsig->aux;
memcpy(p, lp->buf, 4), p += 4;
PBIT32(p, nlmp), p += 8;
for(lp=lumps->l; lp!=lumps; p+=n, lp=lp->l){
n = lp->f->length;
if(lp->buf != nil)
memcpy(p, lp->buf, n);
else
readlump(lp, p, n);
}
PBIT32(l->buf + 8, p - l->buf);
ofs = Nhdr;
for(lp=lumps->l; lp!=lumps; ofs+=n, lp=lp->l){
n = lp->f->length;
PBIT32(p, ofs), p += 4;
PBIT32(p, n), p += 4;
memcpy(p, lp->name, 8), p += 8;
}
dirty = 0;
}
void
fsread(Req *r)
{
vlong n, ofs, end;
File *f;
Lump *l;
f = r->fid->file;
l = f->aux;
ofs = r->ifcall.offset + l->ofs;
end = l->ofs + f->length;
n = r->ifcall.count;
if(ofs + n >= end)
n = end - ofs;
if(n <= 0){
r->ofcall.count = 0;
respond(r, nil);
return;
}
if(f == fwad && dirty)
makewad();
if(l->buf != nil)
memcpy(r->ofcall.data, l->buf+ofs, n);
else{
Bseek(wad, ofs, 0);
n = Bread(wad, r->ofcall.data, n);
if(n < 0){
responderror(r);
return;
}
}
accessfile(f, AREAD);
r->ofcall.count = n;
respond(r, nil);
}
int
addlump(Lump *l, File *dir)
{
Lump *lp;
lp = lumps->lp;
if(dir != fs.tree->root){
lp = dir->aux;
lp = lp->type == LTmap ? sortmap(lp, l) : lastlump(lp);
}
if(l->type == LTend && lp->l->type == LTend && lp->l->f->parent == dir){
werrstr("an end marker already exists");
return -1;
}
link(l, lp, 0);
if(l->type == LTmrk){
strcat(l->name, "_START");
if(endldir(l, l) < 0)
return -1;
}else if(l->type == LTreg){
l->buf = emalloc9p(Nbuf);
l->nbuf = Nbuf;
}
dirty = 1;
return 0;
}
Lump *
createlump(char *s, File *dir, int ismark)
{
int type;
Lump *l;
if(!validname(s, dir, &type, 1, ismark))
return nil;
l = emalloc9p(sizeof *l);
l->type = type;
strupr(l->name, s);
return l;
}
void
fscreate(Req *r)
{
int p;
File *f;
Lump *l;
f = r->fid->file;
p = r->ifcall.perm;
if(p & DMDIR)
p = p & ~0777 | p & f->mode & 0777;
else
p = p & ~0666 | p & f->mode & 0666;
l = createlump(r->ifcall.name, f, p & DMDIR);
if(l == nil)
goto err;
f = createfile(f, r->ifcall.name, r->fid->uid, p, l);
if(f == nil){
free(l);
goto err;
}
l->f = f;
if(addlump(l, r->fid->file) < 0){
removefile(f);
goto err;
}
r->fid->file = f;
r->ofcall.qid = f->qid;
respond(r, nil);
return;
err:
responderror(r);
}
void
fsopen(Req *r)
{
File *f;
f = r->fid->file;
if((f->mode & DMAPPEND) == 0 && (r->ifcall.mode & OTRUNC) != 0
&& f != fsig){
fwad->length -= f->length;
f->length = 0;
dirty = 1;
}
respond(r, nil);
}
void
fsdestroyfile(File *f)
{
freelump(f->aux);
}
Srv fs = {
.open = fsopen,
.create = fscreate,
.read = fsread,
.write = fswrite,
.remove = fsremove,
.wstat = fswstat
};
int
get32(Biobuf *bf, u32int *v)
{
int n;
uchar u[4];
n = Bread(bf, u, sizeof u);
if(n != sizeof u)
return -1;
*v = GBIT32(u);
return 0;
}
File *
replacefile(File *dir, char *fname, int mode, Lump *l)
{
File *f;
incref(dir);
f = walkfile(dir, fname);
if(f == nil)
return nil;
if(removefile(f) < 0)
return nil;
f = createfile(dir, fname, nil, mode, l);
return f;
}
void
addsigfile(char *sig)
{
int n;
Lump *l;
File *f;
n = strlen(sig) + 1;
l = emalloc9p(sizeof *l);
l->buf = (uchar *)estrdup9p(sig);
l->buf[n-1] = '\n';
f = createfile(fs.tree->root, "SIG", nil, rdonly ? 0444 : 0666, l);
if(f == nil)
sysfatal("addsigfile: %r");
else{
fsig = f;
f->length = n;
}
}
void
addwadfile(void)
{
Lump *l;
File *f;
l = emalloc9p(sizeof *l);
f = createfile(fs.tree->root, "WAD", nil, 0444, l);
if(f == nil)
sysfatal("addwadfile: %r");
else{
fwad = f;
f->length = Nhdr;
}
dirty++;
}
void
checkends(void)
{
Lump *lp;
if(ldir == fs.tree->root)
return;
lp = ldir->aux;
if(lp->type != LTmap && endldir(lp, lastlump(lp)) < 0)
fprint(2, "checkends: %r\n");
ldir = ldir->parent;
checkends();
}
int
addfile(Lump *l, u32int *len, int mode)
{
int err;
char fname[sizeof l->name], *s;
Lump *lp;
File *f;
*len = 0;
if(get32(wad, &l->ofs) < 0 || get32(wad, len) < 0)
return -1;
if(Bread(wad, l->name, sizeof(l->name)-1) != sizeof(l->name)-1)
return -1;
strlwr(fname, l->name);
lp = ldir->aux;
err = !validname(fname, ldir, &l->type, 0, 0);
switch(l->type){
case LTmap:
closefile(ldir);
ldir = fs.tree->root;
if(err && lp != nil && lp->type != LTmap){
fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, *len);
if(endldir(lp, lastlump(lp)) < 0)
fprint(2, "endldir: %r\n");
}
mode |= DMDIR|0111;
*len = 0;
break;
case LTmrk:
if(err){
if(lp != nil && lp->type == LTmap){
closefile(ldir);
ldir = fs.tree->root;
}else{
fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, *len);
if(endldir(lp, lastlump(lp)) < 0)
return -1;
ldir = ldir->parent;
}
}
s = strrchr(fname, '_');
*s = 0;
mode |= DMDIR|0111;
*len = 0;
break;
case LTend:
if(err){
ldir = ldir->parent;
return -1;
}
*len = 0;
break;
case LTreg:
if(err){
if(ismaplump(fname))
fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, *len);
else
ldir = fs.tree->root;
}
break;
default:
return -1;
}
f = createfile(ldir, fname, nil, mode, l);
if(f == nil){
fprint(2, "createfile %s: %r\n", l->name);
if(mode & DMDIR)
return -1;
f = replacefile(ldir, fname, mode, l);
if(f == nil)
return -1;
}
if(mode & DMDIR)
ldir = f;
else if(l->type == LTend)
ldir = ldir->parent;
else
closefile(f);
f->length = *len;
l->f = f;
return 0;
}
void
parsewad(void)
{
int n, ne, mode;
u32int len;
Lump *l;
mode = rdonly ? 0444 : 0666;
ldir = fs.tree->root;
for(n=0, ne=nlmp, nlmp=0; n<ne; n++){
l = emalloc9p(sizeof *l);
if(addfile(l, &len, mode) < 0){
fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, len);
free(l);
continue;
}
link(l, lumps->lp, len);
}
checkends();
}
void
wadinfo(char *sig)
{
int n;
u32int dictofs;
n = Bread(wad, sig, Nsig);
if(n != Nsig)
sysfatal("readwad: short read: %r");
sig[4] = 0;
if(strcmp(sig, "IWAD") != 0 && strcmp(sig, "PWAD") != 0)
sysfatal("invalid wad signature");
if(get32(wad, &nlmp) < 0 || get32(wad, &dictofs) < 0)
sysfatal("wadinfo: %r");
Bseek(wad, dictofs, 0);
}
void
usage(void)
{
fprint(2, "usage: %s [-Dr] [-m mtpt] [-S srvname] [wad]\n", argv0);
exits("usage");
}
void
main(int argc, char **argv)
{
int fl, p;
char *mtpt, *srvname, sig[Nsig+1] = "PWAD";
mtpt = "/mnt/wad";
srvname = nil;
fl = MREPL|MCREATE;
p = DMDIR|0777;
ARGBEGIN{
case 'D': chatty9p++; break;
case 'S': srvname = EARGF(usage()); break;
case 'm': mtpt = EARGF(usage()); break;
case 'r': rdonly++; p &= ~0222; fl &= ~MCREATE; break;
default: usage();
}ARGEND
if(*argv != nil){
wad = Bopen(*argv, OREAD);
if(wad == nil)
sysfatal("Bopen: %r");
wadinfo(sig);
}
fs.tree = alloctree(nil, nil, p, fsdestroyfile);
addsigfile(sig);
addwadfile();
parsewad();
postmountsrv(&fs, srvname, mtpt, fl);
exits(nil);
}