Scott Haseley 0ab862abfa 1) Moved tprintf and related functions to their own file in common/libc/printf/tprintf.c.
This file is included by both libc and hostcompat.

2) Changed printf -> tprintf in all testbin programs
2016-01-15 13:33:11 -05:00

2912 lines
66 KiB
C

/*
* Copyright (c) 2013, 2015
* The President and Fellows of Harvard College.
* Written by David A. Holland.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <assert.h>
#include <errno.h>
#include <err.h>
#include "name.h"
#include "data.h"
#include "pool.h"
#include "check.h"
/*
* Check pass.
*
* We first replay the workload into a model that keeps track of what
* should be on the volume at each step; then we inspect the real
* volume and compare it to the model.
*/
////////////////////////////////////////////////////////////
// model representation
#define UNKNOWN_ID ((unsigned)-1)
/*
* 1. log of changes to a filesystem
* (this is not quite fully general but supports only things we do)
*/
enum fschanges {
FC_NEWFS, /* create the volume */
FC_TRUNCATE, /* truncate a file */
FC_WRITE, /* write to a file */
FC_CREAT, /* create a file */
FC_MKDIR, /* create a directory */
FC_RMDIR, /* remove a directory */
FC_UNLINK, /* remove a file */
FC_LINK, /* hardlink a file */
FC_RENAMEFILE, /* rename a file */
FC_RENAMEDIR, /* rename a directory */
};
struct fschange {
/* all changes are kept in order on a doubly linked list */
struct fschange *prev;
struct fschange *next;
/* version number for this change */
unsigned version;
/* whether this change reflects a partially committed operation */
int partial;
/* the change type, parameters, and metadata */
enum fschanges type;
union {
struct {
/* object id of the root directory */
unsigned rootdirnum;
} fc_newfs;
struct {
/* cached: previous change affecting this file */
struct fschange *prev_thisfile;
/* truncate params: file oid and new length */
unsigned file;
off_t len;
} fc_truncate;
struct {
/* cached: previous change affecting this file */
struct fschange *prev_thisfile;
/* write params: file oid, position, length */
unsigned file;
off_t pos;
off_t len;
/* size of file before the write */
off_t oldfilesize;
/* key for generating the write contents */
unsigned code;
unsigned seq;
} fc_write;
struct {
/* cached: previous change affecting containing dir */
struct fschange *prev_thisdir;
/* creat params: containing dir oid, name, new oid */
unsigned dir;
unsigned name;
unsigned newfile;
} fc_creat;
struct {
/* cached: previous change affecting containing dir */
struct fschange *prev_thisdir;
/* mkdir params: containing dir oid, name, new oid */
unsigned dir;
unsigned name;
unsigned newdir;
} fc_mkdir;
struct {
/* cached: previous change affecting either dir */
struct fschange *prev_thisdir;
struct fschange *prev_victimdir;
/* rmdir params */
unsigned dir;
unsigned name;
unsigned victimdir;
} fc_rmdir;
struct {
/* cached: previous change affecting either object */
struct fschange *prev_thisdir;
struct fschange *prev_victimfile;
/* unlink params */
unsigned dir;
unsigned name;
unsigned victimfile;
} fc_unlink;
struct {
/* cached: previous change affecting each object */
struct fschange *prev_fromdir;
struct fschange *prev_todir;
struct fschange *prev_thisfile;
/* link params */
unsigned fromdir;
unsigned fromname;
unsigned todir;
unsigned toname;
unsigned file;
} fc_link;
struct {
/* cached: previous change affecting each object */
struct fschange *prev_fromdir;
struct fschange *prev_todir;
struct fschange *prev_movedfile;
/* rename params */
unsigned fromdir;
unsigned fromname;
unsigned todir;
unsigned toname;
unsigned movedfile;
} fc_renamefile;
struct {
/* cached: previous change affecting each object */
struct fschange *prev_fromdir;
struct fschange *prev_todir;
struct fschange *prev_moveddir;
/* rename params */
unsigned fromdir;
unsigned fromname;
unsigned todir;
unsigned toname;
unsigned moveddir;
} fc_renamedir;
};
};
/*
* 2. representation of a current (visible) filesystem state
*
* The state starts at the root directory; each directory is a singly
* linked list of directory entries, each of which holds another
* directory or a file. We don't keep track of file contents, only the
* length.
*
* The identity field is within the union structure because file and
* directory object ids are different namespaces.
*/
struct fsdirent {
unsigned name;
struct fsobject *obj;
struct fsdirent *next;
};
struct fsobject {
unsigned isdir;
unsigned refcount;
union {
struct {
unsigned identity;
off_t len;
} obj_file;
struct {
unsigned identity;
struct fsdirent *entries;
struct fsobject *parent;
} obj_dir;
};
};
////////////////////////////////////////////////////////////
// allocator pools
/*
* Allocate from static space instead of using malloc, to help avoid
* unnecessary lossage on kernels where malloc doesn't work.
*/
#define MAXCHANGES 16384
#define MAXOBJECTS 16384
#define MAXDIRENTS 16384
DECLPOOL(fschange, MAXCHANGES);
DECLPOOL(fsobject, MAXOBJECTS);
DECLPOOL(fsdirent, MAXDIRENTS);
static
struct fschange *
getchange(void)
{
return POOLALLOC(fschange);
}
static
struct fsobject *
getobject(void)
{
return POOLALLOC(fsobject);
}
static
struct fsdirent *
getdirent(void)
{
return POOLALLOC(fsdirent);
}
#if 0
static
void
putchange(struct fschange *fc)
{
POOLFREE(fschange, fc);
}
#endif
static
void
putobject(struct fsobject *obj)
{
POOLFREE(fsobject, obj);
}
static
void
putdirent(struct fsdirent *dirent)
{
POOLFREE(fsdirent, dirent);
}
////////////////////////////////////////////////////////////
// record constructors for change records
static
struct fschange *
fc_create(enum fschanges type)
{
struct fschange *fc;
fc = getchange();
fc->prev = fc->next = NULL;
fc->version = 0;
fc->partial = 0;
fc->type = type;
return fc;
}
static
struct fschange *
fc_create_newfs(unsigned rootdirnum)
{
struct fschange *fc;
fc = fc_create(FC_NEWFS);
fc->fc_newfs.rootdirnum = rootdirnum;
return fc;
}
static
struct fschange *
fc_create_truncate(struct fschange *prev_thisfile, unsigned file, off_t len)
{
struct fschange *fc;
fc = fc_create(FC_TRUNCATE);
fc->fc_truncate.prev_thisfile = prev_thisfile;
fc->fc_truncate.file = file;
fc->fc_truncate.len = len;
return fc;
}
static
struct fschange *
fc_create_write(struct fschange *prev_thisfile, unsigned file,
off_t pos, off_t len, off_t oldfilesize,
unsigned code, unsigned seq)
{
struct fschange *fc;
fc = fc_create(FC_WRITE);
fc->fc_write.prev_thisfile = prev_thisfile;
fc->fc_write.file = file;
fc->fc_write.pos = pos;
fc->fc_write.len = len;
fc->fc_write.oldfilesize = oldfilesize;
fc->fc_write.code = code;
fc->fc_write.seq = seq;
return fc;
}
static
struct fschange *
fc_create_creat(struct fschange *prev_thisdir,
unsigned dir, unsigned name, unsigned newfile)
{
struct fschange *mdc;
mdc = fc_create(FC_CREAT);
mdc->fc_creat.prev_thisdir = prev_thisdir;
mdc->fc_creat.dir = dir;
mdc->fc_creat.name = name;
mdc->fc_creat.newfile = newfile;
return mdc;
}
static
struct fschange *
fc_create_mkdir(struct fschange *prev_thisdir,
unsigned dir, unsigned name, unsigned newdir)
{
struct fschange *mdc;
mdc = fc_create(FC_MKDIR);
mdc->fc_mkdir.prev_thisdir = prev_thisdir;
mdc->fc_mkdir.dir = dir;
mdc->fc_mkdir.name = name;
mdc->fc_mkdir.newdir = newdir;
return mdc;
}
static
struct fschange *
fc_create_rmdir(struct fschange *prev_thisdir, struct fschange *prev_victimdir,
unsigned dir, unsigned name, unsigned victimdir)
{
struct fschange *mdc;
mdc = fc_create(FC_RMDIR);
mdc->fc_rmdir.prev_thisdir = prev_thisdir;
mdc->fc_rmdir.prev_victimdir = prev_victimdir;
mdc->fc_rmdir.dir = dir;
mdc->fc_rmdir.name = name;
mdc->fc_rmdir.victimdir = victimdir;
return mdc;
}
static
struct fschange *
fc_create_unlink(struct fschange *prev_thisdir,
struct fschange *prev_victimfile,
unsigned dir, unsigned name, unsigned victimfile)
{
struct fschange *mdc;
mdc = fc_create(FC_UNLINK);
mdc->fc_unlink.prev_thisdir = prev_thisdir;
mdc->fc_unlink.prev_victimfile = prev_victimfile;
mdc->fc_unlink.dir = dir;
mdc->fc_unlink.name = name;
mdc->fc_unlink.victimfile = victimfile;
return mdc;
}
static
struct fschange *
fc_create_link(struct fschange *prev_fromdir, struct fschange *prev_todir,
struct fschange *prev_thisfile,
unsigned fromdir, unsigned fromname,
unsigned todir, unsigned toname,
unsigned file)
{
struct fschange *mdc;
mdc = fc_create(FC_LINK);
mdc->fc_link.prev_fromdir = prev_fromdir;
mdc->fc_link.prev_todir = prev_todir;
mdc->fc_link.prev_thisfile = prev_thisfile;
mdc->fc_link.fromdir = fromdir;
mdc->fc_link.fromname = fromname;
mdc->fc_link.todir = todir;
mdc->fc_link.toname = toname;
mdc->fc_link.file = file;
return mdc;
}
static
struct fschange *
fc_create_renamefile(struct fschange *prev_fromdir,
struct fschange *prev_todir,
struct fschange *prev_movedfile,
unsigned fromdir, unsigned fromname,
unsigned todir, unsigned toname,
unsigned movedfile)
{
struct fschange *mdc;
mdc = fc_create(FC_RENAMEFILE);
mdc->fc_renamefile.prev_fromdir = prev_fromdir;
mdc->fc_renamefile.prev_todir = prev_todir;
mdc->fc_renamefile.prev_movedfile = prev_movedfile;
mdc->fc_renamefile.fromdir = fromdir;
mdc->fc_renamefile.fromname = fromname;
mdc->fc_renamefile.todir = todir;
mdc->fc_renamefile.toname = toname;
mdc->fc_renamefile.movedfile = movedfile;
return mdc;
}
static
struct fschange *
fc_create_renamedir(struct fschange *prev_fromdir,
struct fschange *prev_todir,
struct fschange *prev_moveddir,
unsigned fromdir, unsigned fromname,
unsigned todir, unsigned toname,
unsigned moveddir)
{
struct fschange *fc;
fc = fc_create(FC_RENAMEDIR);
fc->fc_renamedir.prev_fromdir = prev_fromdir;
fc->fc_renamedir.prev_todir = prev_todir;
fc->fc_renamedir.prev_moveddir = prev_moveddir;
fc->fc_renamedir.fromdir = fromdir;
fc->fc_renamedir.fromname = fromname;
fc->fc_renamedir.todir = todir;
fc->fc_renamedir.toname = toname;
fc->fc_renamedir.moveddir = moveddir;
return fc;
}
////////////////////////////////////////////////////////////
// constructors/destructors for current state objects
static
struct fsdirent *
fsdirent_create(unsigned name, struct fsobject *obj)
{
struct fsdirent *ret;
ret = getdirent();
ret->name = name;
ret->obj = obj;
ret->next = NULL;
return ret;
}
static
void
fsdirent_destroy(struct fsdirent *de)
{
assert(de->obj == NULL);
assert(de->next == NULL);
putdirent(de);
}
static
struct fsobject *
fsobject_create_file(unsigned id)
{
struct fsobject *ret;
ret = getobject();
ret->isdir = 0;
ret->refcount = 1;
ret->obj_file.identity = id;
ret->obj_file.len = 0;
return ret;
}
static
struct fsobject *
fsobject_create_dir(unsigned id, struct fsobject *parent)
{
struct fsobject *ret;
ret = getobject();
ret->isdir = 1;
ret->refcount = 1;
ret->obj_dir.identity = id;
ret->obj_dir.entries = NULL;
ret->obj_dir.parent = parent;
return ret;
}
static
void
fsobject_incref(struct fsobject *obj)
{
assert(obj->refcount > 0);
obj->refcount++;
assert(obj->refcount > 0);
}
static
void
fsobject_decref(struct fsobject *obj)
{
assert(obj->refcount > 0);
obj->refcount--;
if (obj->refcount > 0) {
return;
}
if (obj->isdir) {
assert(obj->obj_dir.entries == NULL);
}
putobject(obj);
}
static
void
fsobject_destroytree(struct fsobject *obj)
{
struct fsdirent *de;
if (obj->isdir) {
while (obj->obj_dir.entries != NULL) {
de = obj->obj_dir.entries;
obj->obj_dir.entries = de->next;
de->next = NULL;
fsobject_destroytree(de->obj);
de->obj = NULL;
fsdirent_destroy(de);
}
}
fsobject_decref(obj);
}
////////////////////////////////////////////////////////////
// operations on current state objects
/*
* Bail out if something's broken.
*/
static
void
die(const char *fmt, ...)
{
char buf[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
errx(1, "Inconsistency: %s", buf);
}
#if 0 /* not used */
/*
* Count a list of directory entries.
*/
static
unsigned
fsdirent_count(struct fsdirent *de)
{
if (de == NULL) {
return 0;
}
return 1 + fsdirent_count(de->next);
}
#endif
/*
* Add an entry to a directory.
*/
static
void
fsdir_add_entry(struct fsobject *dir, struct fsdirent *nde)
{
struct fsdirent *ode;
assert(dir->isdir);
for (ode = dir->obj_dir.entries; ode != NULL; ode = ode->next) {
if (ode->name == nde->name) {
die("In directory %u, %s already existed",
dir->obj_dir.identity, name_get(nde->name));
}
}
nde->next = dir->obj_dir.entries;
dir->obj_dir.entries = nde;
}
/*
* Find an entry in a directory by name. If CROAK is set, bail out if
* it's not there.
*/
static
struct fsdirent *
fsdir_find_entry(struct fsobject *dir, unsigned name, int croak)
{
struct fsdirent *de;
assert(dir->isdir);
for (de = dir->obj_dir.entries; de != NULL; de = de->next) {
if (de->name == name) {
return de;
}
}
if (croak) {
die("In directory %u, did not find %s",
dir->obj_dir.identity, name_get(name));
}
return NULL;
}
/*
* Remove an entry from a directory.
*/
static
struct fsdirent *
fsdir_remove_entry(struct fsobject *dir, unsigned name)
{
struct fsdirent *de, **prevptr;
assert(dir->isdir);
prevptr = &dir->obj_dir.entries;
for (de = *prevptr; de != NULL; prevptr = &de->next, de = *prevptr) {
if (de->name == name) {
*prevptr = de->next;
de->next = NULL;
return de;
}
}
die("In directory %u, did not find %s",
dir->obj_dir.identity, name_get(name));
return NULL;
}
////////////////////////////////////////////////////////////
// apply a change record to a current state
static
struct fsobject *
findsub(struct fsobject *obj, unsigned isdir, unsigned id)
{
struct fsobject *ret;
struct fsdirent *de;
unsigned objid;
/* Are we on the object we're looking for? */
objid = obj->isdir ? obj->obj_dir.identity : obj->obj_file.identity;
if (obj->isdir == isdir && objid == id) {
return obj;
}
/* If the object we're on isn't a dir, can't search any further */
if (!obj->isdir) {
return NULL;
}
for (de = obj->obj_dir.entries; de != NULL; de = de->next) {
ret = findsub(de->obj, isdir, id);
if (ret != NULL) {
return ret;
}
}
return NULL;
}
#if 0
static
struct fsobject *
findfile(struct fsobject *rootdir, unsigned id)
{
struct fsobject *ret;
ret = findsub(rootdir, 0/*!isdir*/, id);
if (ret == NULL) {
die("File %u not found in current state", id);
}
return ret;
}
#endif
static
struct fsobject *
findfile_maybe(struct fsobject *rootdir, unsigned id)
{
return findsub(rootdir, 0/*!isdir*/, id);
}
static
struct fsobject *
finddir(struct fsobject *rootdir, unsigned id)
{
struct fsobject *ret;
ret = findsub(rootdir, 1/*isdir*/, id);
if (ret == NULL) {
die("Directory %u not found in current state", id);
}
return ret;
}
/*
* Apply change CHANGE to the volume state encoded in/under ROOTDIR.
*/
static
void
apply_change(struct fsobject *rootdir, struct fschange *change)
{
struct fsobject *obj1, *obj2;
struct fsdirent *de;
off_t endpos;
assert(rootdir->isdir);
switch (change->type) {
case FC_NEWFS:
/*
* Creating the volume; crosscheck the root directory.
*/
assert(rootdir->isdir);
assert(rootdir->refcount == 1);
assert(rootdir->obj_dir.identity ==
change->fc_newfs.rootdirnum);
assert(rootdir->obj_dir.entries == NULL);
assert(rootdir->obj_dir.parent == rootdir);
break;
case FC_TRUNCATE:
/*
* Truncate a file. (Only files, not directories.)
*
* Because truncates can and do get posted after a
* file is unlinked, we have to tolerate not finding
* the file. We don't model unlinked files, because
* they aren't visible and our goal is to check the
* visible/observable state.
*/
obj1 = findfile_maybe(rootdir, change->fc_truncate.file);
if (obj1 != NULL) {
obj1->obj_file.len = change->fc_truncate.len;
}
break;
case FC_WRITE:
/*
* Write to a file. (Only files, not directories.)
* All this actually does is update the file size if
* needed.
*
* We also have to tolerate writes to unlinked files.
*/
obj1 = findfile_maybe(rootdir, change->fc_write.file);
if (obj1 != NULL) {
endpos = change->fc_write.pos + change->fc_write.len;
if (obj1->obj_file.len < endpos) {
obj1->obj_file.len = endpos;
}
}
break;
case FC_CREAT:
/*
* Create a file. This creates a new object and a
* directory entry to hold it, then inserts the
* directory entry in the containing directory.
*/
obj1 = finddir(rootdir, change->fc_creat.dir);
obj2 = fsobject_create_file(change->fc_creat.newfile);
de = fsdirent_create(change->fc_creat.name, obj2);
fsdir_add_entry(obj1, de);
break;
case FC_MKDIR:
/*
* Create a directory. The same as creating a file,
* except the new object is a directory.
*/
obj1 = finddir(rootdir, change->fc_mkdir.dir);
obj2 = fsobject_create_dir(change->fc_mkdir.newdir, obj1);
de = fsdirent_create(change->fc_mkdir.name, obj2);
fsdir_add_entry(obj1, de);
break;
case FC_RMDIR:
/*
* Remove a directory. First take out the directory
* entry (by name); then examine the object found;
* then delete everything.
*
* XXX: why do these checks use assert vs. testing and
* calling die()?
*/
obj1 = finddir(rootdir, change->fc_rmdir.dir);
de = fsdir_remove_entry(obj1, change->fc_rmdir.name);
obj2 = de->obj;
de->obj = NULL;
assert(obj2->isdir);
assert(obj2->obj_dir.entries == NULL);
assert(obj2->obj_dir.identity == change->fc_rmdir.victimdir);
assert(obj2->obj_dir.parent == obj1);
fsdirent_destroy(de);
fsobject_decref(obj2);
break;
case FC_UNLINK:
/*
* Remove a file. Much the same as removing a directory.
*/
obj1 = finddir(rootdir, change->fc_unlink.dir);
de = fsdir_remove_entry(obj1, change->fc_unlink.name);
obj2 = de->obj;
de->obj = NULL;
assert(!obj2->isdir);
assert(obj2->obj_file.identity ==
change->fc_unlink.victimfile);
fsdirent_destroy(de);
fsobject_decref(obj2);
break;
case FC_LINK:
/*
* Hard-link a file.
*/
obj1 = finddir(rootdir, change->fc_link.fromdir);
de = fsdir_find_entry(obj1, change->fc_link.fromname, 1);
obj2 = de->obj;
assert(!obj2->isdir);
assert(obj2->obj_file.identity == change->fc_link.file);
obj1 = finddir(rootdir, change->fc_link.todir);
fsobject_incref(obj2);
de = fsdirent_create(change->fc_link.toname, obj2);
fsdir_add_entry(obj1, de);
break;
case FC_RENAMEFILE:
/*
* Rename a file. XXX: this appears to do the wrong
* thing if you rename one file over another.
*/
obj1 = finddir(rootdir, change->fc_renamefile.fromdir);
de = fsdir_remove_entry(obj1, change->fc_renamefile.fromname);
obj2 = de->obj;
assert(!obj2->isdir);
assert(obj2->obj_file.identity ==
change->fc_renamefile.movedfile);
obj1 = finddir(rootdir, change->fc_renamefile.todir);
de->name = change->fc_renamefile.toname;
fsdir_add_entry(obj1, de);
break;
case FC_RENAMEDIR:
/*
* Rename a directory. Same as renaming a file, except
* that we update the parent of the directory being
* moved. XXX: also seems to do the wrong thing if you
* rename one directory over another. And, XXX: should
* this be updating the refcount as the parent
* changes? Shouldn't the refcount be the same as the
* on-disk linkcount, and shouldn't the on-disk
* linkcount be part of the state we examine and
* verify?
*/
obj1 = finddir(rootdir, change->fc_renamedir.fromdir);
de = fsdir_remove_entry(obj1, change->fc_renamedir.fromname);
obj2 = de->obj;
assert(obj2->isdir);
assert(obj2->obj_dir.identity ==
change->fc_renamedir.moveddir);
assert(obj2->obj_dir.parent == obj1);
obj1 = finddir(rootdir, change->fc_renamedir.todir);
de->name = change->fc_renamedir.toname;
obj2->obj_dir.parent = obj1;
fsdir_add_entry(obj1, de);
break;
}
}
////////////////////////////////////////////////////////////
// global fs state
/*
* The change records. FIRSTCHANGE is the first change record; CHANGES
* is the list root, which given the current linked list implementation
* is the *last* (most recent) change record.
*/
static struct fschange *firstchange;
static struct fschange *changes;
/*
* The current volume state that we're working with. As we add change
* records while replaying the workload, we roll this forward so we
* can inspect it; for some things this is necessary to be able to
* replay the workload fully.
*/
static struct fsobject *state;
/*
* The next oids of each type to assign. File and directory ids are
* counted independently, which might or might not have been a good
* idea.
*/
static unsigned nextfilenum, nextdirnum;
/*
* Add a new change record. This allocates the version number;
* however, the change record should otherwise be fully filled in
* already.
*
* usage: fc_attach(newrecord);
*/
static
void
fc_attach(struct fschange *new)
{
struct fschange *prev;
prev = changes;
assert(prev->next == NULL);
assert(new->prev == NULL);
assert(new->next == NULL);
prev->next = new;
new->prev = prev;
new->version = prev->version + 1;
changes = new;
apply_change(state, new);
}
/*
* Rewind the volume state kept in the global STATE to the beginning.
*/
static
void
rewindstate(void)
{
/* If we already have a state, throw it away. */
if (state != NULL) {
fsobject_destroytree(state);
}
assert(firstchange->type == FC_NEWFS);
state = fsobject_create_dir(firstchange->fc_newfs.rootdirnum, NULL);
/* root directory's parent is itself */
state->obj_dir.parent = state;
}
/*
* Roll the volume state kept in STATE forward to a specific change
* entry.
*/
static
void
advancestateto(struct fschange *targetchange)
{
struct fschange *change;
for (change = firstchange; change != NULL; change = change->next) {
apply_change(state, change);
if (change == targetchange) {
return;
}
}
assert(0);
}
////////////////////////////////////////////////////////////
// lookup in the fs state
// (used during model construction)
/*
* Find the most recent previous record that mentions a particular file.
*
* If there isn't one, something's wrong; because we start with a
* fresh volume every file that ever exists must have been created at
* some point.
*
* This is used while replaying the workload and accumulating change
* records, so it can always just start from the most recent end
* instead of from a specific place.
*/
static
struct fschange *
changes_findprevfile(unsigned filenum)
{
struct fschange *here;
for (here = changes; here != NULL; here = here->prev) {
switch (here->type) {
case FC_NEWFS:
break;
case FC_TRUNCATE:
if (here->fc_truncate.file == filenum) {
return here;
}
break;
case FC_WRITE:
if (here->fc_write.file == filenum) {
return here;
}
break;
case FC_CREAT:
if (here->fc_creat.newfile == filenum) {
return here;
}
break;
case FC_MKDIR:
case FC_RMDIR:
break;
case FC_UNLINK:
if (here->fc_unlink.victimfile == filenum) {
return here;
}
break;
case FC_LINK:
if (here->fc_link.file == filenum) {
return here;
}
break;
case FC_RENAMEFILE:
if (here->fc_renamefile.movedfile == filenum) {
return here;
}
break;
case FC_RENAMEDIR:
break;
}
}
die("No previous record for file %u", filenum);
return NULL;
}
/*
* Find the most recent previous record that mentions a particular dir.
*
* Likewise, if there isn't one, something's wrong.
*/
static
struct fschange *
changes_findprevdir(unsigned dirnum)
{
struct fschange *here;
for (here = changes; here != NULL; here = here->prev) {
switch (here->type) {
case FC_NEWFS:
if (here->fc_newfs.rootdirnum == dirnum) {
return here;
}
break;
case FC_TRUNCATE:
case FC_WRITE:
break;
case FC_CREAT:
if (here->fc_creat.dir == dirnum) {
return here;
}
break;
case FC_MKDIR:
if (here->fc_mkdir.dir == dirnum ||
here->fc_mkdir.newdir == dirnum) {
return here;
}
break;
case FC_RMDIR:
if (here->fc_rmdir.dir == dirnum ||
here->fc_rmdir.victimdir == dirnum) {
return here;
}
break;
case FC_UNLINK:
if (here->fc_unlink.dir == dirnum) {
return here;
}
break;
case FC_LINK:
if (here->fc_link.fromdir == dirnum ||
here->fc_link.todir == dirnum) {
return here;
}
break;
case FC_RENAMEFILE:
if (here->fc_renamefile.fromdir == dirnum ||
here->fc_renamefile.todir == dirnum) {
return here;
}
break;
case FC_RENAMEDIR:
if (here->fc_renamedir.fromdir == dirnum ||
here->fc_renamedir.todir == dirnum ||
here->fc_renamedir.moveddir == dirnum) {
return here;
}
break;
}
}
die("No previous record for directory %u", dirnum);
return NULL;
}
#if 0
/*
* Back up to the previous record mentioning the same file.
*
* XXX: No longer used and should be removed.
*/
static
struct fschange *
fs_prevforfile(struct fschange *fc, unsigned filenum)
{
switch (fc->type) {
case FC_TRUNCATE:
if (fc->fc_truncate.file == filenum) {
return fc->fc_truncate.prev_thisfile;
}
break;
case FC_WRITE:
if (fc->fc_write.file == filenum) {
return fc->fc_write.prev_thisfile;
}
break;
case FC_UNLINK:
if (fc->fc_unlink.victimfile == filenum) {
return fc->fc_unlink.prev_victimfile;
}
break;
case FC_LINK:
if (fc->fc_link.file == filenum) {
return fc->fc_link.prev_thisfile;
}
break;
case FC_RENAMEFILE:
if (fc->fc_renamefile.movedfile == filenum) {
return fc->fc_renamefile.prev_movedfile;
}
break;
default:
break;
}
return fc->prev;
}
#endif
#if 0
/*
* Back up to the previous record mentioning the same directory.
*
* XXX: No longer used and should be removed.
*/
static
struct fschange *
fs_prevfordir(struct fschange *fc, unsigned dirnum)
{
switch (fc->type) {
case FC_CREAT:
if (fc->fc_creat.dir == dirnum) {
return fc->fc_creat.prev_thisdir;
}
break;
case FC_MKDIR:
if (fc->fc_mkdir.dir == dirnum) {
return fc->fc_mkdir.prev_thisdir;
}
break;
case FC_RMDIR:
if (fc->fc_rmdir.dir == dirnum) {
return fc->fc_rmdir.prev_thisdir;
}
if (fc->fc_rmdir.victimdir == dirnum) {
return fc->fc_rmdir.prev_victimdir;
}
break;
case FC_UNLINK:
if (fc->fc_unlink.dir == dirnum) {
return fc->fc_unlink.prev_thisdir;
}
break;
case FC_LINK:
if (fc->fc_link.fromdir == dirnum) {
return fc->fc_link.prev_fromdir;
}
if (fc->fc_link.todir == dirnum) {
return fc->fc_link.prev_todir;
}
break;
case FC_RENAMEFILE:
if (fc->fc_renamefile.fromdir == dirnum) {
return fc->fc_renamefile.prev_fromdir;
}
if (fc->fc_renamefile.todir == dirnum) {
return fc->fc_renamefile.prev_todir;
}
break;
case FC_RENAMEDIR:
if (fc->fc_renamedir.fromdir == dirnum) {
return fc->fc_renamedir.prev_fromdir;
}
if (fc->fc_renamedir.todir == dirnum) {
return fc->fc_renamedir.prev_todir;
}
if (fc->fc_renamedir.moveddir == dirnum) {
return fc->fc_renamedir.prev_moveddir;
}
break;
default:
break;
}
return fc->prev;
}
/*
* Find an object by name and containing directory by searching the
* change history.
*
* No longer used (instead we maintain the current state and search
* that, which is a lot simpler) and should probably be removed.
*/
struct findresult {
int isfile;
unsigned objnum;
};
static
int
fs_find(unsigned dirnum, unsigned name, struct findresult *res)
{
struct fschange *here;
for (here = fs; here != NULL; here = fs_prevfordir(here, dirnum)) {
switch (here->type) {
case FC_NEWFS:
case FC_TRUNCATE:
case FC_WRITE:
break;
case FC_CREAT:
if (here->fc_creat.dir == dirnum &&
here->fc_creat.name == name) {
res->isfile = 1;
res->objnum = here->fc_creat.newfile;
return 0;
}
break;
case FC_MKDIR:
if (here->fc_mkdir.dir == dirnum &&
here->fc_mkdir.name == name) {
res->isfile = 0;
res->objnum = here->fc_mkdir.newdir;
return 0;
}
if (here->fc_mkdir.newdir == dirnum) {
return -1;
}
break;
case FC_RMDIR:
if (here->fc_rmdir.dir == dirnum &&
here->fc_rmdir.name == name) {
return -1;
}
if (here->fc_rmdir.victimdir == dirnum) {
return -1;
}
break;
case FC_UNLINK:
if (here->fc_unlink.dir == dirnum &&
here->fc_unlink.name == name) {
return -1;
}
break;
case FC_LINK:
if ((here->fc_link.todir == dirnum &&
here->fc_link.toname == name) ||
(here->fc_link.fromdir == dirnum &&
here->fc_link.fromname == name)) {
res->isfile = 1;
res->objnum = here->fc_link.file;
return 0;
}
break;
case FC_RENAMEFILE:
if (here->fc_renamefile.fromdir == dirnum &&
here->fc_renamefile.fromname == name) {
return -1;
}
if (here->fc_renamefile.todir == dirnum &&
here->fc_renamefile.toname == name) {
res->isfile = 1;
res->objnum = here->fc_renamefile.movedfile;
return 0;
}
break;
case FC_RENAMEDIR:
if (here->fc_renamedir.fromdir == dirnum &&
here->fc_renamedir.fromname == name) {
return -1;
}
if (here->fc_renamedir.todir == dirnum &&
here->fc_renamedir.toname == name) {
res->isfile = 0;
res->objnum = here->fc_renamedir.moveddir;
return 0;
}
break;
}
}
return -1;
}
/*
* Find a file by name and containing directory, using fs_find to
* search the history.
*
* No longer used; should be removed.
*/
static
unsigned
fs_findfile(unsigned dirnum, unsigned name)
{
struct findresult res;
if (fs_find(dirnum, name, &res) < 0) {
die("In directory %u, did not find %s",
dirnum, name_get(name));
}
if (res.isfile == 0) {
die("In directory %u, %s was a directory",
dirnum, name_get(name));
}
return res.objnum;
}
/*
* Find a directory by name and containing directory, using fs_find to
* search the history.
*
* No longer used; should be removed.
*/
static
unsigned
fs_finddir(unsigned dirnum, unsigned name)
{
struct findresult res;
if (fs_find(dirnum, name, &res) < 0) {
die("In directory %u, did not find %s",
dirnum, name_get(name));
}
if (res.isfile == 1) {
die("In directory %u, %s was not a directory",
dirnum, name_get(name));
}
return res.objnum;
}
/*
* Check if a name (in a specific containing directory) is a file,
* using fs_find.
*
* No longer used; should be removed.
*/
static
int
fs_isfile(unsigned dirnum, unsigned name)
{
struct findresult res;
if (fs_find(dirnum, name, &res) < 0) {
return 0;
}
return res.isfile;
}
/*
* Check if a name (in a specific containing directory) is a
* directory, using fs_find.
*
* No longer used; should be removed.
*/
static
int
fs_isdir(unsigned dirnum, unsigned name)
{
struct findresult res;
if (fs_find(dirnum, name, &res) < 0) {
return 0;
}
return !res.isfile;
}
/*
* Find the parent of a directory by its object id, by searching the
* history.
*
* No longer used; should be removed.
*/
static
unsigned
fs_findparent(unsigned dirnum)
{
struct fschange *here;
for (here = fs; here != NULL; here = fs_prevfordir(here, dirnum)) {
switch (here->type) {
case FC_NEWFS:
if (here->fc_newfs.rootdirnum == dirnum) {
return dirnum;
}
break;
case FC_TRUNCATE:
case FC_WRITE:
case FC_CREAT:
break;
case FC_MKDIR:
if (here->fc_mkdir.newdir == dirnum) {
return here->fc_mkdir.dir;
}
break;
case FC_RMDIR:
if (here->fc_rmdir.victimdir == dirnum) {
die("Directory %u was removed", dirnum);
}
break;
case FC_UNLINK:
case FC_LINK:
case FC_RENAMEFILE:
break;
case FC_RENAMEDIR:
if (here->fc_renamedir.moveddir == dirnum) {
return here->fc_renamedir.todir;
}
break;
}
}
die("Directory %u not found", dirnum);
return 0;
}
#endif /* 0 */
/*
* Find a file by name and containing directory, by searching the
* volume state.
*/
static
unsigned
model_findfile(unsigned dirnum, unsigned name)
{
struct fsobject *obj;
struct fsdirent *de;
obj = finddir(state, dirnum);
de = fsdir_find_entry(obj, name, 1);
if (de->obj->isdir) {
die("In directory %u, %s was a directory",
dirnum, name_get(name));
}
return de->obj->obj_file.identity;
}
/*
* Find a directory by name and containing directory, by searching the
* volume state.
*/
static
unsigned
model_finddir(unsigned dirnum, unsigned name)
{
struct fsobject *obj;
struct fsdirent *de;
obj = finddir(state, dirnum);
de = fsdir_find_entry(obj, name, 1);
if (!de->obj->isdir) {
die("In directory %u, %s was not a directory",
dirnum, name_get(name));
}
return de->obj->obj_dir.identity;
}
/*
* Find a directory's parent by object id, by searching the volume
* state.
*/
static
unsigned
model_findparent(unsigned dirnum)
{
struct fsobject *obj;
obj = finddir(state, dirnum);
assert(obj->isdir);
assert(obj->obj_dir.parent->isdir);
return obj->obj_dir.parent->obj_dir.identity;
}
/*
* Check if a name (in a specific containing directory) is a file, by
* searching the volume state.
*/
static
unsigned
model_isfile(unsigned dirnum, unsigned name)
{
struct fsobject *obj;
struct fsdirent *de;
obj = finddir(state, dirnum);
de = fsdir_find_entry(obj, name, 0);
return de != NULL && !de->obj->isdir;
}
/*
* Check if a name (in a specific containing directory) is a
* directory, by searching the volume state.
*/
static
unsigned
model_isdir(unsigned dirnum, unsigned name)
{
struct fsobject *obj;
struct fsdirent *de;
obj = finddir(state, dirnum);
de = fsdir_find_entry(obj, name, 0);
return de != NULL && de->obj->isdir;
}
/*
* Get the current size of a file in the current volume state.
*/
static
off_t
model_getfilesize(unsigned filenum)
{
struct fsobject *obj;
obj = findfile_maybe(state, filenum);
if (obj == NULL) {
/* file is unlinked */
return 0;
}
assert(!obj->isdir);
return obj->obj_file.len;
}
////////////////////////////////////////////////////////////
// model construction (replaying the workload)
/* The workload's current directory. */
static unsigned cwd;
/*
* Set up for replying the workload: allocate the root directory,
* generate the newfs record, set up the volume state, initialize
* the current directory.
*/
void
check_setup(void)
{
unsigned rootdir;
assert(firstchange == NULL);
assert(changes == NULL);
assert(state == NULL);
assert(nextfilenum == 0);
assert(nextdirnum == 0);
rootdir = nextdirnum++;
firstchange = changes = fc_create_newfs(rootdir);
rewindstate();
/* apply the first change (the newfs record) */
apply_change(state, changes);
cwd = rootdir;
}
/*
* Workload replay: create a file
*/
int
check_createfile(unsigned name)
{
struct fschange *prevdir;
unsigned filenum;
prevdir = changes_findprevdir(cwd);
filenum = nextfilenum++;
fc_attach(fc_create_creat(prevdir, cwd, name, filenum));
return filenum;
}
/*
* Workload replay: open a file
*/
int
check_openfile(unsigned name)
{
return model_findfile(cwd, name);
}
/*
* Workload replay: close a file
*/
void
check_closefile(int handle, unsigned name)
{
/* don't need to do anything */
(void)handle;
(void)name;
}
/*
* Workload replay: write to a file
*
* CODE and SEQ encode the contents to be written.
*/
void
check_write(int handle, unsigned name, unsigned code, unsigned seq,
off_t pos, off_t len)
{
unsigned filenum;
struct fschange *prevfile;
off_t prevlen;
filenum = handle;
assert(filenum < nextfilenum);
(void)name;
prevlen = model_getfilesize(filenum);
prevfile = changes_findprevfile(filenum);
fc_attach(fc_create_write(prevfile, filenum, pos, len, prevlen,
code, seq));
}
/*
* Workload replay: truncate a file
*/
void
check_truncate(int handle, unsigned name, off_t len)
{
unsigned filenum;
struct fschange *prevfile;
filenum = handle;
assert(filenum < nextfilenum);
(void)name;
prevfile = changes_findprevfile(filenum);
fc_attach(fc_create_truncate(prevfile, filenum, len));
}
/*
* Workload replay: create a directory
*/
void
check_mkdir(unsigned name)
{
struct fschange *prevdir;
unsigned dirnum;
prevdir = changes_findprevdir(cwd);
dirnum = nextdirnum++;
fc_attach(fc_create_mkdir(prevdir, cwd, name, dirnum));
}
/*
* Workload replay: remove a directory
*/
void
check_rmdir(unsigned name)
{
struct fschange *prevdir, *prevvictim;
unsigned victim;
prevdir = changes_findprevdir(cwd);
victim = model_finddir(cwd, name);
prevvictim = changes_findprevdir(victim);
fc_attach(fc_create_rmdir(prevdir, prevvictim, cwd, name, victim));
}
/*
* Workload replay: remove a file
*/
void
check_unlink(unsigned name)
{
struct fschange *prevdir, *prevvictim;
unsigned victim;
prevdir = changes_findprevdir(cwd);
victim = model_findfile(cwd, name);
prevvictim = changes_findprevfile(victim);
fc_attach(fc_create_unlink(prevdir, prevvictim, cwd, name, victim));
}
/*
* Workload replay: hard link a file
*/
void
check_link(unsigned fromname, unsigned toname)
{
struct fschange *prevdir, *prevfile;
unsigned filenum;
prevdir = changes_findprevdir(cwd);
filenum = model_findfile(cwd, fromname);
prevfile = changes_findprevfile(filenum);
fc_attach(fc_create_link(prevdir, prevdir, prevfile,
cwd, fromname, cwd, toname, filenum));
}
/*
* Workload replay: rename something
*/
static
void
common_rename(unsigned fromdirnum, unsigned fromname,
unsigned todirnum, unsigned toname)
{
struct fschange *prevfromdir, *prevtodir, *prevfrom, *prevto;
unsigned fromnum, tonum;
struct fschange *fc;
int isfile;
prevfromdir = changes_findprevdir(fromdirnum);
prevtodir = changes_findprevdir(todirnum);
if (model_isfile(todirnum, toname)) {
isfile = 1;
assert(model_isfile(fromdirnum, fromname));
tonum = model_findfile(todirnum, toname);
prevto = changes_findprevfile(tonum);
fc = fc_create_unlink(prevtodir, prevto,
todirnum, toname, tonum);
}
else if (model_isdir(todirnum, toname)) {
isfile = 0;
assert(model_isdir(fromdirnum, fromname));
tonum = model_finddir(todirnum, toname);
prevto = changes_findprevdir(tonum);
fc = fc_create_rmdir(prevtodir, prevto,
todirnum, toname, tonum);
}
else {
isfile = model_isfile(fromdirnum, fromname);
fc = NULL;
}
if (fc) {
fc->partial = 1;
fc_attach(fc);
}
if (isfile) {
fromnum = model_findfile(fromdirnum, fromname);
prevfrom = changes_findprevfile(fromnum);
fc = fc_create_renamefile(prevfromdir, prevtodir, prevfrom,
fromdirnum, fromname,
todirnum, toname, fromnum);
}
else {
fromnum = model_finddir(fromdirnum, fromname);
prevfrom = changes_findprevdir(fromnum);
fc = fc_create_renamedir(prevfromdir, prevtodir, prevfrom,
fromdirnum, fromname,
todirnum, toname, fromnum);
}
fc_attach(fc);
}
/*
* Workload replay: rename within the current directory
*/
void
check_rename(unsigned from, unsigned to)
{
common_rename(cwd, from, cwd, to);
}
/*
* Workload replay: cross-directory rename
*/
void
check_renamexd(unsigned fromdir, unsigned from, unsigned todir, unsigned to)
{
unsigned fromdirnum, todirnum;
/* fromdir/todir are names in cwd */
fromdirnum = model_finddir(cwd, fromdir);
todirnum = model_finddir(cwd, todir);
common_rename(fromdirnum, from, todirnum, to);
}
/*
* Workload replay: change directory
*/
void
check_chdir(unsigned name)
{
cwd = model_finddir(cwd, name);
}
/*
* Workload replay: change directory to ..
*/
void
check_chdirup(void)
{
cwd = model_findparent(cwd);
}
/*
* Workload replay: sync
*/
void
check_sync(void)
{
/* nothing */
}
////////////////////////////////////////////////////////////
// inspection of the fs
/* the root of the state we found (as opposed to the state we modeled) */
static struct fsobject *found;
/* count of how much we found */
static unsigned found_subdirs, found_files;
/*
* Wrapper. As of this writing OS/161 provides fstat but not stat...
* grr
*/
static
int
xstat(const char *path, struct stat *buf)
{
int fd, ret, serrno;
fd = open(path, O_RDONLY);
if (fd < 0) {
return -1;
}
ret = fstat(fd, buf);
serrno = errno;
close(fd);
errno = serrno;
return ret;
}
/*
* Inspect a directory. PARENTOBJ is the *containing* directory;
* PARENTINO is its inode number, and DIRNAMESTR is the name of the
* directory to inspect. The arguments are set up this way (rather
* than e.g. passing the fsobject of the directory to inspect) to
* improve the error reporting, and to allow checking .., and so
* forth.
*/
static
struct fsobject *
inspectdir(struct fsobject *parentobj, ino_t parentino, const char *dirnamestr)
{
char subnamestr[NAME_MAX+1];
size_t subnamelen;
struct stat dirstat, dotstat, substat;
int dirfd;
struct fsobject *subobj, *ret;
struct fsdirent *contents, *de;
/*
* Stat the target, and cd into it.
*/
if (xstat(dirnamestr, &dirstat)) {
err(1, "%s: stat", dirnamestr);
}
assert(S_ISDIR(dirstat.st_mode));
if (chdir(dirnamestr)) {
err(1, "%s: chdir", dirnamestr);
}
/*
* Check that . is correct
*/
if (xstat(".", &dotstat)) {
err(1, "In %s: .: stat", dirnamestr);
}
if (dotstat.st_dev != dirstat.st_dev) {
errx(1, "in %s: .: wrong volume id; found %u, expected %u",
dirnamestr, dotstat.st_dev, dirstat.st_dev);
}
if (dotstat.st_ino != dirstat.st_ino) {
errx(1, "%s/.: wrong inode number; found %u, expected %u",
dirnamestr, dotstat.st_ino, dirstat.st_ino);
}
/*
* Check that .. leads back
*/
if (xstat("..", &dotstat)) {
err(1, "In %s: ..: stat", dirnamestr);
}
if (dotstat.st_dev != dirstat.st_dev) {
errx(1, "In %s: ..: wrong volume id; found %u, expected %u",
dirnamestr, dotstat.st_dev, dirstat.st_dev);
}
if (dotstat.st_ino != parentino) {
errx(1, "In %s: ..: wrong inode number; found %u, expected %u",
dirnamestr, dotstat.st_ino, parentino);
}
/*
* Create a directory fsobject.
*/
dirfd = open(".", O_RDONLY);
if (dirfd < 0) {
err(1, "In %s: .: open", dirnamestr);
}
ret = fsobject_create_dir(UNKNOWN_ID, parentobj);
/*
* Read the contents of the target directory and create
* directory entries for them. Recurse for ones that are
* themselves directories.
*/
contents = NULL;
while (1) {
subnamelen = getdirentry(dirfd, subnamestr,
sizeof(subnamestr)-1);
if (subnamelen == 0) {
break;
}
subnamestr[subnamelen] = 0;
if (!strcmp(subnamestr, ".") || !strcmp(subnamestr, "..")) {
continue;
}
if (xstat(subnamestr, &substat)) {
err(1, "In %s: %s: stat", dirnamestr, subnamestr);
}
if (S_ISDIR(substat.st_mode)) {
subobj = inspectdir(ret, dirstat.st_ino, subnamestr);
found_subdirs++;
}
else {
subobj = fsobject_create_file(UNKNOWN_ID);
subobj->obj_file.len = substat.st_size;
found_files++;
}
de = fsdirent_create(name_find(subnamestr), subobj);
de->next = contents;
contents = de;
}
/*
* Done scanning; cd out, save the contents, and return the
* new object.
*/
close(dirfd);
if (chdir("..")) {
err(1, "In %s; ..: chdir", dirnamestr);
}
ret->obj_dir.entries = contents;
return ret;
}
/*
* Inspect the whole volume by starting with "." -- we assume that we
* were run in the root directory.
*/
static
void
inspectfs(void)
{
struct stat st;
if (xstat(".", &st)) {
err(1, ".: stat");
}
found = inspectdir(NULL, st.st_ino, ".");
}
////////////////////////////////////////////////////////////
// comparison of state trees
/*
* Count the number of objects at and below a particular fsobject.
*/
static
unsigned
count_subtree(struct fsobject *obj)
{
struct fsdirent *de;
unsigned ret = 1;
if (obj->isdir) {
for (de = obj->obj_dir.entries; de != NULL; de = de->next) {
ret += count_subtree(de->obj);
}
}
return ret;
}
/*
* Compare two fsobjects. Return the matching score. (lower scores are
* better matches)
*/
static
unsigned
compare_objects(struct fsobject *obja, struct fsobject *objb)
{
struct fsdirent *enta, *entb;
unsigned ret, found;
if (obja->isdir != objb->isdir) {
/*
* One object's a file, the other's a directory.
*
* Return one point for each name in the missing
* subtree. This includes one point for the top dir,
* which is mismatched rather than missing.
*/
if (obja->isdir) {
return count_subtree(obja);
}
assert(objb->isdir);
return count_subtree(objb);
}
if (!obja->isdir) {
/*
* Both objects are files
*/
assert(!objb->isdir);
if (obja->obj_file.len != objb->obj_file.len) {
/* one point for the size being different */
return 1;
}
return 0;
}
/*
* Both objects are directories. Recurse on all pairs of
* entries that have the same name. Return one point for each
* name that's present (recursively) in one object but not the
* other.
*
* XXX: sort the entries first or something instead of naively
* searching 2*N^2 times.
*/
assert(obja->isdir);
assert(objb->isdir);
ret = 0;
for (enta = obja->obj_dir.entries; enta != NULL; enta = enta->next) {
found = 0;
for (entb = objb->obj_dir.entries; entb != NULL;
entb = entb->next) {
if (enta->name == entb->name) {
ret += compare_objects(enta->obj, entb->obj);
found = 1;
break;
}
}
if (!found) {
if (enta->obj->isdir) {
/* whole subtree is missing */
ret += count_subtree(enta->obj);
}
/* one file is missing */
ret += 1;
}
}
for (entb = objb->obj_dir.entries; entb != NULL; entb = entb->next) {
found = 0;
for (enta = obja->obj_dir.entries; enta != NULL;
enta = enta->next) {
if (enta->name == entb->name) {
found = 1;
break;
}
}
if (!found) {
if (entb->obj->isdir) {
/* whole subtree is missing */
ret += count_subtree(entb->obj);
}
/* one file is missing */
ret += 1;
}
}
return ret;
}
/*
* Print an indentation.
*/
static
void
doindent(unsigned depth)
{
unsigned i;
for (i=0; i<depth; i++) {
tprintf(" ");
}
}
/*
* Print out the differences between two fsobjects. Otherwise much the
* same as compare_objects.
*
* OBJA is "expected" (the model); OBJB is "found" (what we saw on disk).
*/
static
void
printdiffs(unsigned indent, struct fsobject *obja, struct fsobject *objb)
{
struct fsdirent *enta, *entb;
unsigned found;
assert(obja->isdir);
assert(objb->isdir);
for (enta = obja->obj_dir.entries; enta != NULL; enta = enta->next) {
found = 0;
for (entb = objb->obj_dir.entries; entb != NULL;
entb = entb->next) {
if (enta->name == entb->name) {
doindent(indent);
tprintf("%s", name_get(enta->name));
if (enta->obj->isdir &&
!entb->obj->isdir) {
tprintf(": expected dir, found file;");
tprintf(" %u names missing.\n",
count_subtree(enta->obj) - 1);
}
else if (!enta->obj->isdir &&
entb->obj->isdir) {
tprintf(": expected file, found dir;");
tprintf(" %u extra names.\n",
count_subtree(entb->obj) - 1);
}
else if (!enta->obj->isdir &&
!entb->obj->isdir) {
off_t alen, blen;
alen = enta->obj->obj_file.len;
blen = entb->obj->obj_file.len;
if (alen == blen) {
tprintf("\t\t%lld bytes (ok)\n",
alen);
}
else {
tprintf(": found %lld bytes, "
"expected %lld "
"bytes.\n",
blen, alen);
}
}
else {
tprintf("/\n");
printdiffs(indent + 1,
enta->obj, entb->obj);
}
found = 1;
break;
}
}
if (!found) {
doindent(indent);
tprintf("%s: missing ", name_get(enta->name));
if (enta->obj->isdir) {
tprintf("subtree with %u names.\n",
count_subtree(enta->obj) - 1);
}
else {
tprintf("file\n");
}
}
}
for (entb = objb->obj_dir.entries; entb != NULL; entb = entb->next) {
found = 0;
for (enta = obja->obj_dir.entries; enta != NULL;
enta = enta->next) {
if (enta->name == entb->name) {
found = 1;
break;
}
}
if (!found) {
doindent(indent);
tprintf("%s: extra ", name_get(entb->name));
if (entb->obj->isdir) {
tprintf("subtree with %u names.\n",
count_subtree(entb->obj) - 1);
}
else {
tprintf("file\n");
}
}
}
}
////////////////////////////////////////////////////////////
// comparison of file contents
/*
* Return the version of the oldest change with the same directory
* structure as CHANGE. This skips past truncate and write operations
* that only change file sizes.
*/
static
unsigned
findokversion(struct fschange *change)
{
while (change != NULL) {
switch (change->type) {
case FC_TRUNCATE:
case FC_WRITE:
break;
default:
return change->version;
}
change = change->prev;
}
assert(0);
return 0;
}
/*
* Back up to previous change entries until we find one affecting the
* contents of the specified file. (That means: truncate, write, or
* create.)
*
* Returns the current (passed-in) change if that matches.
*/
static
struct fschange *
backup_for_file(struct fschange *change, unsigned filenum)
{
while (change != NULL) {
switch (change->type) {
case FC_TRUNCATE:
if (change->fc_truncate.file == filenum) {
return change;
}
break;
case FC_WRITE:
if (change->fc_write.file == filenum) {
return change;
}
break;
case FC_CREAT:
if (change->fc_creat.newfile == filenum) {
return change;
}
break;
default:
break;
}
change = change->prev;
}
return NULL;
}
/*
* Expect zeros in a file from START to END. The file is given by FD
* and named NAMESTR. Report if we find stuff other than zeros.
*/
static
void
checkfilezeros(int fd, const char *namestr, off_t start, off_t end)
{
char buf[1024];
size_t len, i;
ssize_t ret;
unsigned poison = 0, trash = 0;
off_t origstart = start;
tprintf(" %lld - %lld (expecting zeros)\n", start, end);
if (lseek(fd, start, SEEK_SET) == -1) {
err(1, "%s: lseek to %lld", namestr, start);
}
while (start < end) {
/* XXX this assumes end - start fits in size_t */
len = end - start;
if (len > sizeof(buf)) {
len = sizeof(buf);
}
ret = read(fd, buf, len);
if (ret == -1) {
err(1, "%s: read %u at %lld", namestr, len, start);
}
if (ret == 0) {
errx(1, "%s: read %u at %lld: Unexpected EOF",
namestr, len, start);
}
for (i=0; i<(size_t)ret; i++) {
if ((unsigned char)buf[i] == POISON_VAL) {
poison++;
}
else if (buf[i] != 0) {
trash++;
}
}
start += ret;
}
if (poison > 0 || trash > 0) {
tprintf("ERROR: File %s: expected zeros from %lld to %lld; "
"found",
namestr, origstart, end);
if (poison > 0) {
tprintf(" %u poison bytes", poison);
if (trash > 0) {
tprintf(" and");
}
}
if (trash > 0) {
tprintf(" %u trash bytes", trash);
}
tprintf("\n");
}
}
/*
* Read data in from a file, into data.c's read buffer where it will
* be checked for correctness. The data.c region involved is
* REGIONSTART..REGIONEND; all of that space is needed so the expected
* data can be generated correctly. However, we're only looking at the
* range CHECKSTART..CHECKEND, which may be only part of the data.c
* region.
*/
static
void
readfiledata(int fd, const char *namestr,
off_t regionstart, off_t checkstart,
off_t checkend, off_t regionend)
{
char *readbuf;
size_t bufpos, len;
ssize_t ret;
/* CHECKSTART..CHECKEND must be within REGIONSTART..REGIONEND */
assert(regionstart <= checkstart);
assert(checkstart <= checkend);
assert(checkend <= regionend);
readbuf = data_mapreadbuf(regionend - regionstart);
bufpos = checkstart - regionstart;
len = checkend - checkstart;
if (lseek(fd, checkstart, SEEK_SET) == -1) {
err(1, "%s: lseek to %lld", namestr, checkstart);
}
while (len > 0) {
ret = read(fd, readbuf + bufpos, len);
if (ret == -1) {
err(1, "%s: read %u at %lld",
namestr, len, regionstart + bufpos);
}
if (ret == 0) {
errx(1, "%s: read %u at %lld: Unexpected EOF",
namestr, len, regionstart + bufpos);
}
bufpos += ret;
assert(len >= (size_t)ret);
len -= ret;
}
}
/*
* Check the data found in the file FD named NAMESTR, in the range
* CHECKSTART..CHECKEND, which is within or equal to the data.c
* region REGIONSTART..REGIONEND. ZEROSTART is the place where the
* data can legitimately be zeroed rather than the expected data
* (the portion of a write that extended a file) and the data is
* generated from CODE and SEQ.
*/
static
void
checkfiledata(int fd, const char *namestr, unsigned code, unsigned seq,
off_t zerostart,
off_t regionstart, off_t checkstart,
off_t checkend, off_t regionend)
{
if (checkstart < regionstart) {
checkstart = regionstart;
}
if (checkend > regionend) {
checkend = regionend;
}
tprintf(" %lld - %lld\n", checkstart, checkend);
readfiledata(fd, namestr,
regionstart, checkstart, checkend, regionend);
data_check(namestr, regionstart,
code, seq, zerostart - regionstart, regionend - regionstart,
checkstart - regionstart, checkend - checkstart);
}
/*
* Check a range of the file FD named NAMESTR, from START to END,
* against the model state expected as of CHANGE.
*/
static
void
checkfilerange(int fd, const char *namestr, struct fschange *change,
off_t start, off_t end)
{
assert(start < end);
if (change->type == FC_TRUNCATE) {
off_t tlen;
struct fschange *prev;
/*
* The most recent change was a truncate. Anything
* beyond the truncate length should read as zeroes;
* recurse on the part before (if any) using the
* previous change affecting this file.
*
* We might be checking that a chunk of the file
* beyond the truncate length is zero (even though as
* of this change it's past EOF) if a later change
* made that region into a hole in a sparse file.
*/
tlen = change->fc_truncate.len;
prev = change->fc_truncate.prev_thisfile;
if (tlen < start) {
checkfilezeros(fd, namestr, start, end);
}
else if (tlen < end) {
checkfilerange(fd, namestr, prev, start, tlen);
checkfilezeros(fd, namestr, tlen, end);
}
else {
checkfilerange(fd, namestr, prev, start, end);
}
}
else if (change->type == FC_WRITE) {
off_t wstart, wend;
struct fschange *prev;
unsigned code, seq;
off_t oldfilesize, zerostart;
/*
* The most recent change was a write.
*/
wstart = change->fc_write.pos;
wend = change->fc_write.pos + change->fc_write.len;
prev = change->fc_write.prev_thisfile;
code = change->fc_write.code;
seq = change->fc_write.seq;
oldfilesize = change->fc_write.oldfilesize;
/*
* ZEROSTART (where we begin to allow the file to
* contain zeros) should be the point at the write
* where we began to extend the file, if any.
*/
if (oldfilesize < wstart) {
zerostart = wstart;
}
else if (oldfilesize < wend) {
zerostart = oldfilesize;
}
else {
zerostart = wend;
}
/*
* Six cases for the range overlap:
* (1) (2) (3) (4) (5) (6)
* ** *** ****** * *** **
* *** *** *** *** *** ***
*
* We call checkfilerange recursively using the
* previous change for this file for the
* non-overlapping parts, because this write didn't
* touch those so they must reflect the previous file
* state; and checkfiledata for the overlapping parts
* that this write did touch.
*/
if (end <= wstart || start >= wend) {
/* cases 1 and 6 */
checkfilerange(fd, namestr, prev, start, end);
}
else {
/* cases 2-5 */
if (start < wstart) {
/* case 2 or 3 */
checkfilerange(fd, namestr, prev,
start, wstart);
}
checkfiledata(fd, namestr, code, seq, zerostart,
wstart, start, end, wend);
if (end > wend) {
/* cases 3 or 5 */
checkfilerange(fd, namestr, prev, wend, end);
}
}
}
else if (change->type == FC_RENAMEFILE) {
struct fschange *prev;
/*
* The most recent change was a rename. As rename
* doesn't affect contents, recurse on the previous
* change affecting the file.
*/
prev = change->fc_renamefile.prev_movedfile;
checkfilerange(fd, namestr, prev, start, end);
}
else {
assert(change->type == FC_CREAT);
/*
* The most recent change was the file being created.
* Like with truncate, check that the range is zero
* even though it's past EOF, as a later write might
* have converted past-EOF space into a hole in a
* sparse file.
*/
checkfilezeros(fd, namestr, start, end);
}
}
/*
* Check if a change CHANGE to a file is present in the observed file
* FD, whose name is NAMESTR and size is FILESIZE.
*
* For writes it checks the data, so should be accurate; for truncate
* all it can check is the size, so it can readily be fooled by
* sequences of truncates or multiple truncates to the same length.
*/
static
int
change_is_present(int fd, const char *namestr, off_t filesize,
struct fschange *change)
{
off_t pos, len, oldfilesize, zerostart;
unsigned code, seq;
switch (change->type) {
case FC_TRUNCATE:
return filesize == change->fc_truncate.len;
case FC_WRITE:
pos = change->fc_write.pos;
len = change->fc_write.len;
code = change->fc_write.code;
seq = change->fc_write.seq;
oldfilesize = change->fc_write.oldfilesize;
if (oldfilesize < pos) {
zerostart = 0;
}
else if (oldfilesize < pos + len) {
zerostart = oldfilesize - pos;
}
else {
zerostart = len;
}
readfiledata(fd, namestr, pos, pos, pos+len, pos+len);
return data_matches(namestr, pos,
code, seq, zerostart, len, 0, len);
case FC_CREAT:
return 1;
default:
break;
}
assert(0);
return 0;
}
/*
* Check the contents of the file called NAMESTR, which is the model
* file FILE, as of change CHANGE.
*/
static
void
checkonefilecontents(const char *namestr, struct fsobject *file,
struct fschange *change)
{
unsigned okversion;
int fd;
/*
* Open the file.
*/
assert(!file->isdir);
fd = open(namestr, O_RDONLY);
if (fd < 0) {
err(1, "%s: open", namestr);
}
/*
* Find the oldest version that has the same directory
* structure as CHANGE, and thus reflects the earliest
* contents we can legitimately find in the file.
*
* XXX: the matching we do to pick the change to examine takes
* file sizes into account, but findokversion specifically
* doesn't stop going backwards if just the file size changes.
* This seems wrong: if we match on file size, ok versions to
* see are only those that have the same file size.
*/
okversion = findokversion(change);
/*
* Find the first change (going backwards) that affects this
* file. There should always be at least the create. This
* change might be before or after OKVERSION.
*/
change = backup_for_file(change, file->obj_file.identity);
if (change == NULL) {
die("File %s was never even created?", namestr);
}
if (file->obj_file.len == 0) {
/*
* The model expects the length to be 0.
*
* XXX: I think the code here was written thinking
* FILE is from the inspection results; but it's not,
* it's the model from the workload replay. So these
* checks appear to be wrong.
*/
if (change->type == FC_CREAT) {
/* file was created empty and never written to */
return;
}
if (change->type == FC_TRUNCATE) {
/* if the length is wrong we shouldn't get this far */
assert(change->fc_truncate.len == 0);
/* so, nothing to check */
close(fd);
return;
}
assert(change->type == FC_WRITE);
tprintf("ERROR: File %s is zero length but was expected to "
"contain at least %lld bytes at offset %lld!\n",
namestr, change->fc_write.pos, change->fc_write.len);
close(fd);
return;
}
/* XXX: this check is wrong too. */
if (change->type == FC_CREAT) {
tprintf("ERROR: File %s was never written to but has "
"length %lld\n",
namestr, file->obj_file.len);
close(fd);
return;
}
/*
* If CHANGE isn't reflected in the file, back up. If this
* causes us to back up past OKVERSION, complain that the
* change should be present.
*
* XXX: the call to change_is_present should be using the
* observed file size, not the modeled file size.
*
* XXX: More seriously, however, this logic is not really
* right. If the workload contains multiple writes that don't
* affect the file length, interspersed with truncates that
* are all to the same length, we'll stop looking back at the
* first truncate (because the length matches) regardless of
* which writes are present. None of the workloads currently
* does anything like this, but it's still wrong.
*
* I think the right thing to do (especially since the version
* matching checks file lengths) is to check only writes for
* presence and ignore truncates; and probably, rather than
* back up based on what writes appear to be present and then
* call checkfilerange, which will then complain loudly if
* parts of an older write didn't make it out, divide the file
* into ranges based on blocks and writes and figure out which
* version the data found in each such range corresponds to,
* then object only to the ones that are crazy and warn about
* ones that reflect data buffers not making it out. But this
* is a fairly big rewrite.
*/
while (!change_is_present(fd, namestr, file->obj_file.len, change)) {
if (change->version < okversion) {
tprintf("File %s: change for version %u is missing\n",
namestr, change->version);
}
change = backup_for_file(change->prev,file->obj_file.identity);
if (change == NULL) {
tprintf("File %s: no matching version found\n",
namestr);
close(fd);
return;
}
}
/*
* Now we've found a version that we think the file contents
* should correspond to; check that it's what we actually
* have.
*
* XXX: should this use the model length or the observed
* length?
*/
checkfilerange(fd, namestr, change, 0, file->obj_file.len);
close(fd);
}
/*
* Check the contents of all files under DIR with respect to the
* change CHANGE. Recurses on subdirectories.
*/
static
void
checkallfilecontents(struct fsobject *dir, struct fschange *change)
{
struct fsdirent *de;
const char *namestr;
assert(dir->isdir);
for (de = dir->obj_dir.entries; de != NULL; de = de->next) {
namestr = name_get(de->name);
if (de->obj->isdir) {
tprintf(" >>> Entering %s\n", namestr);
if (chdir(namestr)) {
err(1, "%s: chdir", namestr);
}
checkallfilecontents(de->obj, change);
tprintf(" <<< Leaving %s\n", namestr);
if (chdir("..")) {
err(1, "..: chdir");
}
}
else {
tprintf("%s...\n", namestr);
checkonefilecontents(namestr, de->obj, change);
}
}
}
////////////////////////////////////////////////////////////
// model validation
/*
* Compare the on-disk state we see to the model we've built.
*/
void
checkfs(void)
{
struct fschange *change, *best;
unsigned score, bestscore;
/*
* We just built the model; talk about it.
*/
tprintf("Established %u versions across %u directories and %u files\n",
changes->version + 1, nextdirnum, nextfilenum);
/*
* Inspect the volume state we've got. Initializes the global
* FOUND holding the found volume state.
*/
inspectfs();
tprintf("Found %u subdirs and %u files on the volume\n",
found_subdirs, found_files);
/*
* Rewind the model state to the beginning.
*/
rewindstate();
/*
* Loop through all the changes; apply each one to the model
* state, and score the model state against the found state.
* Keep track of the best matching version.
*
* XXX: there might be several versions with the same score,
* in which case we'll blindly take the most recent one. If
* there are several versions that match exactly (which can
* easily happen for workloads that don't explicitly avoid it)
* we'll also blindly take the most recent one, and then the
* matching of file contents can blow up. We should collect
* all the versions with the same score, and score each one
* on file contents too. Or something like that.
*/
change = firstchange;
assert(change->type == FC_NEWFS);
best = NULL;
bestscore = 0;
while (change != NULL) {
apply_change(state, change);
score = compare_objects(state, found);
if (best == NULL || score <= bestscore) {
best = change;
bestscore = score;
}
//tprintf("version %u score %u\n", change->version, score);
change = change->next;
}
assert(best != NULL);
/*
* Set the model state to the best matching state we found.
*/
rewindstate();
advancestateto(best);
if (bestscore > 0) {
/*
* We didn't get an exact match, so print how the
* differences. XXX: this results in not checking file
* data...
*/
tprintf("FAILURE: Directory tree does not match on any "
"version.\n");
tprintf("Best version is %u; describing differences:\n",
best->version);
printdiffs(1, state, found);
return;
}
/*
* Ok, we did get an exact match. Print it.
*/
tprintf("Directory tree matched in version %u.\n", best->version);
if (best->partial) {
tprintf("WARNING: this is a version from a partially committed "
"operation.\n");
}
/*
* XXX we should check hard links here; that is, that all the
* files that are supposed to be hard links of each other are,
* and that no other files are randomly hardlinked.
*
* However, if things are wrong it should result in bad
* contents. It would be very unlikely for even a very broken
* filesystem to, on its own, copy files that are supposed to
* be hardlinked.
*/
/* now check the file contents */
tprintf("Checking file contents...\n");
checkallfilecontents(state, best);
tprintf("Done.\n");
}