/* -*-Mode: C++;-*-
 * PRCS - The Project Revision Control System
 * Copyright (C) 1997  Josh MacDonald
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: projdesc.cc 1.9.1.1.1.4.1.17.1.5.1.17.3.9.1.34.2.1 Tue, 05 Feb 2002 07:58:12 -0800 jmacd $
 */

extern "C" {
#include <stdarg.h>
}

#if __GNUG__ < 3
#define pubsync sync
#endif

#include "prcs.h"
#include "projdesc.h"
#include "sexp.h"
#include "fileent.h"
#include "vc.h"
#include "repository.h"
#include "misc.h"
#include "hash.h"
#include "quick.h"
#include "system.h"
#include "checkout.h"
#include "memseg.h"
#include "setkeys.h"
#include "populate.h"

#define prj_lex_val prjtext
#define prj_lex_len prjleng

/* in attrs.cc */
extern const struct AttrDesc*
is_file_attribute (register const char *str, register int len);

static const char bad_match_help_string[] =
"When trying to determine how files are related between versions, PRCS "
"first attempts to find a file with the same file family, then looks for "
"a file with the same name.  If both exist, and are not the same file, "
"the match is ambiguous.  This can happen if you rename a file and then "
"recreate a file with the original name.  You may do this, but it is "
"problematic when performing merge and diff operations";

extern const char *default_merge_descs[14];
extern const MergeAction default_merge_actions[14];

EXTERN int         prjleng;
EXTERN const char* prjtext;
EXTERN int         prj_lex_this_index;
EXTERN int         prj_lex_cur_index;
EXTERN int         prj_lineno;

EXTERN int is_builtin_keyword(const char* s, int len);

Dstring* ProjectDescriptor::project_version_name()  const { return _project_version_name;}
Dstring* ProjectDescriptor::project_version_major() const { return _project_version_major; }
Dstring* ProjectDescriptor::project_version_minor() const { return _project_version_minor; }
Dstring* ProjectDescriptor::parent_version_name()  const { return _parent_version_name; }
Dstring* ProjectDescriptor::parent_version_major() const { return _parent_version_major; }
Dstring* ProjectDescriptor::parent_version_minor() const { return _parent_version_minor; }
Dstring* ProjectDescriptor::project_description() const { return _project_description;}
Dstring* ProjectDescriptor::version_log()         const { return _version_log; }
Dstring* ProjectDescriptor::new_version_log()     const { return _new_version_log; }
Dstring* ProjectDescriptor::checkin_time()        const { return _checkin_time; }
Dstring* ProjectDescriptor::checkin_login()       const { return _checkin_login; }
Dstring* ProjectDescriptor::created_by_major()    const { return _created_by_major; }
Dstring* ProjectDescriptor::created_by_minor()    const { return _created_by_minor; }
Dstring* ProjectDescriptor::created_by_micro()    const { return _created_by_micro; }
MergeParentEntryPtrArray* ProjectDescriptor::new_merge_parents() const { return _new_merge_parents; }
void     ProjectDescriptor::merge_notify_incomplete() { _merge_incomplete = true; }
void     ProjectDescriptor::merge_notify_complete()   { _merge_complete = true; }
RepEntry   *ProjectDescriptor::repository_entry() const { return _repository_entry; }
void        ProjectDescriptor::repository_entry(RepEntry* rep) { _repository_entry = rep; }
QuickElim  *ProjectDescriptor::quick_elim()        const { return _quick_elim; }
void        ProjectDescriptor::quick_elim(QuickElim* qe) { _quick_elim = qe; }
const char *ProjectDescriptor::project_full_name() const { return _project_full_name; }
const char *ProjectDescriptor::project_name()      const { return _project_name; }
const char *ProjectDescriptor::project_file_path() const { return _project_file_path; }
const char *ProjectDescriptor::project_aux_path()  const { return _project_aux_path; }
const char *ProjectDescriptor::project_path()      const { return _project_path; }
const char *ProjectDescriptor::full_version()      const { return _full_version; }
OrderedStringTable* ProjectDescriptor::populate_ignore() const { return _populate_ignore; };
ProjectVersionData* ProjectDescriptor::project_version_data() const { return _project_version_data; }
FileEntryPtrArray* ProjectDescriptor::file_entries() { return _file_entries; }
PrettyOstream& ProjectDescriptor::merge_log() const { return *_log; }
void        ProjectDescriptor::merge_log(PrettyOstream& str) { _log = &str; }

ProjectDescriptor::~ProjectDescriptor()
{
    if (_segment) delete (_segment);

    if (_file_entries) {
	foreach_fileent (fe_ptr, this)
	    delete (*fe_ptr);

	delete (_file_entries);
    }

    if (_log_pstream) {
        if (_log_pstream->pubsync()) {
	    prcserror << "warning: Write to merge log failed" << perror;
	}

	delete _log_pstream;
	delete _log_stream;
	delete _log;
    }

    if (_all_estrings) {
	foreach (es_ptr, _all_estrings, DstringPtrArray::ArrayIterator)
	    delete (*es_ptr);

	delete _all_estrings;
    }

    delete _deleted_markers;

    delete_merge_parents (_merge_parents);
    delete_merge_parents (_new_merge_parents);

    if (_nullified_chars)  delete _nullified_chars;
    if (_populate_ignore)  delete _populate_ignore;
    if (_quick_elim)       delete _quick_elim;
    if (_project_keywords) delete _project_keywords;
    if (_project_keywords_extra) delete _project_keywords_extra;
    if (_keyword_id)       delete _keyword_id;
    if (_keyword_pheader)  delete _keyword_pheader;
    if (_keyword_pversion) delete _keyword_pversion;
    if (_file_name_table)  delete _file_name_table;
    if (_file_desc_table)  delete _file_desc_table;
    if (_file_match_cache) delete _file_match_cache;
}

ProjectDescriptor::ProjectDescriptor()
    :_prj_source_name(NULL),
     _read_flags(KeepNothing),
     _quick_elim(NULL),
     _project_name(NULL),
     _populate_ignore(NULL),
     _segment(NULL),
     _nullified_chars(NULL),
     _all_estrings(NULL),
     _files_insertion_point(NULL),
     _project_version_point(NULL),
     _end_of_buffer_point(NULL),
     _project_keywords_point(NULL),
     _populate_ignore_point(NULL),
     _merge_parents_marker(0, 0),
     _new_merge_parents_marker(0, 0),
     _descends_from_marker(0, 0),
     _project_keywords_marker(0, 0),
     _populate_ignore_marker(0, 0),
     _deleted_markers(NULL),
     _repository_entry(NULL),
     _file_entries(NULL),
     _project_version_data(NULL),
     _log_pstream(NULL),
     _log_stream(NULL),
     _log(NULL),
     _project_version_name(NULL),
     _project_version_major(NULL),
     _project_version_minor(NULL),
     _parent_version_name(NULL),
     _parent_version_major(NULL),
     _parent_version_minor(NULL),
     _project_description(NULL),
     _version_log(NULL),
     _new_version_log(NULL),
     _checkin_time(NULL),
     _checkin_login(NULL),
     _created_by_major(NULL),
     _created_by_minor(NULL),
     _created_by_micro(NULL),
     _merge_parents(NULL),
     _new_merge_parents(NULL),
     _project_keywords(NULL),
     _project_keywords_extra(NULL),
     _keyword_id(NULL),
     _keyword_pheader(NULL),
     _keyword_pversion(NULL),
     _merge_entry(NULL),
     _merge_selected_major(NULL),
     _merge_selected_minor(NULL),
     _append_merge_parents(false),
     _alter_popkey(false),
     _merge_complete(false),
     _merge_incomplete(false),
     _file_name_table(NULL),
     _file_desc_table(NULL),
     _file_match_cache(NULL) { }

PrVoidError ProjectDescriptor::bad_project_file(const Sexp *s, const char* mes)
{
    pthrow prcserror << fileline(_prj_source_name, s->line_number())
		    << "Invalid project file, " << mes << dotendl;
}

PrProjectDescriptorPtrError read_project_file(const char* project_full_name,
					      const char* filename,
					      bool is_working,
					      ProjectReadData flags)
{
    ProjectDescriptor* new_project = new ProjectDescriptor();

    Return_if_fail(new_project->init_from_file(project_full_name, filename, is_working, NULL, flags));

    return new_project;
}

PrProjectDescriptorPtrError read_project_file(const char* project_full_name,
					      const char* file_name,
					      bool is_working,
					      FILE* infile,
					      ProjectReadData flags)
{
    ProjectDescriptor* new_project = new ProjectDescriptor();

    Return_if_fail(new_project->init_from_file(project_full_name, file_name, is_working, infile, flags));

    return new_project;
}

PrVoidError ProjectDescriptor::init_repository_entry(const char* file, bool lock, bool create)
{
    Return_if_fail(_repository_entry << Rep_init_repository_entry(file, lock, create, true));

    return NoError;
}

PrVoidError ProjectDescriptor::init_from_file(const char* full_name,
					      const char* filename,
					      bool is_working,
					      FILE* infile0,
					      ProjectReadData flags)
{
    const char* last_slash;
    FILE* infile;

    _read_flags = flags;

    _project_full_name.assign(full_name);
    _project_file_path.assign(full_name);
    _project_file_path.append(".prj");

    last_slash = strrchr(full_name, '/');

    if(last_slash) {
	_project_name = last_slash + 1;
	_project_path.assign(full_name);
	_project_path.truncate(last_slash - full_name + 1);
    } else {
	_project_name = full_name;
	/*_project_path.truncate(0);*/
    }

    _project_aux_path.sprintf("%s.%s.prcs_aux", _project_path.cast(), _project_name);

    if(infile0 == NULL) {
	struct stat buf;

        if((infile = fopen(filename, "r")) == NULL)
	    pthrow prcserror << "Can't open file " << squote(filename) << perror;

	If_fail(Err_stat(filename, & buf))
	    pthrow prcserror << "Stat failed on project file "
			     << squote (filename) << perror;

	_read_mode = buf.st_mode & 0777;
    } else {
	infile = infile0;
	_read_mode = 0;
    }

    _prj_source_name = filename;

    if (_read_flags & KeepMergeParents) {
	_new_merge_parents = new MergeParentEntryPtrArray;
	_merge_parents = new MergeParentEntryPtrArray;
    }

    _populate_ignore = new OrderedStringTable;
    _project_keywords = new KeywordTable;
    _project_keywords_extra = new OrderedStringTable;
    _file_entries = new FileEntryPtrArray;
    _deleted_markers = new ConstListMarkerPtrArray;
    _attrs_table = new AttrsTable (attrs_hash, attrs_equal);

    Return_if_fail(parse_prj_file(infile));

    set_full_version (is_working);

    if(infile0 == NULL)
	fclose(infile);

    return NoError;
}

void ProjectDescriptor::set_full_version (bool is_working)
{
    _full_version.assign (*project_version_major());
    _full_version.append ('.');
    _full_version.append (*project_version_minor());

    if (is_working)
	_full_version.append ("(w)");
}

PrVoidError ProjectDescriptor::verify_merge_parents()
{
    foreach (ent_ptr, _merge_parents, MergeParentEntryPtrArray::ArrayIterator) {
	MergeParentEntry *entry = *ent_ptr;

	Return_if_fail (entry->project_data << check_version_name (entry->selected_version, entry->lineno));
    }

    foreach (ent_ptr, _new_merge_parents, MergeParentEntryPtrArray::ArrayIterator) {
	MergeParentEntry *entry = *ent_ptr;

	Return_if_fail (entry->project_data << check_version_name (entry->selected_version, entry->lineno));
    }

    return NoError;
}

PrVoidError ProjectDescriptor::init_merge_log()
{
    const char* logname_env = get_environ_var ("PRCS_MERGE_LOG");
    Dstring logname;

    if (logname_env)
	logname.assign (logname_env);
    else {
	logname.assign (project_full_name());
	logname.append (".log");
    }

    _log_stream = new filebuf;

    if (!_log_stream->open(logname, ios::out|ios::app)) {
	pthrow prcserror << "Failed opening merge log file " << squote(logname) << perror;
    }

    _log_pstream = new PrettyStreambuf (_log_stream, &option_report_actions);

    _log_pstream->set_fill_width (1<<20);
    _log_pstream->set_fill_prefix ("");

    _log = new PrettyOstream (_log_pstream, NoError);

    return NoError;
}

/* format should be a string consisting of 'U', 'N', 'L', 'S', and
 * 'A', for user, number, label, string and any.  returns false if a
 * match fails.  arguments following format are the addresses of
 * Dstring*s to fill in. an example is:
 *
 *     sexp_scan(s, "LLL", &d1, &d2, &d3);
 *
 * where dN will get the Nth label of three.
 *
 * if the first character of format is an asterisk, then "-*-" is
 * accepted as a label.  if the first cahracter of format is an 'e',
 * then empty lists are allowed.  */
static bool sexp_scan(const Sexp* list, const char* format, ...)
{
    va_list args;
    char c;
    int len = list->length();
    const char* type = NULL;
    bool accept_ast = false;
    bool accept_empty = false;
    bool loop_done = false;

    while (!loop_done) {
	switch (*format) {
	case '*':
	    accept_ast = true;
	    format += 1;
	    break;
	case 'e':
	    accept_empty = true;
	    format += 1;
	    break;
	default:
	    loop_done = true;
	    break;
	}
    }

    va_start(args, format);

    if(!list->is_pair()) goto error;

    while((c = *format++)) {
	switch(c) {
	case 'N': type = "number"; break;
	case 'S': type = "any"; break;
	case 'L': type = "label"; break;
	case 'U': type = "login"; break;
	case 'A': type = "any"; break;
	}

	/* If there are no characters left, and we are expecting one, bail */
	if(len == 0) goto error;

	bool okay_empty = false;

	if(list->car()->is_pair()) {
	    if (accept_empty && !list->car())
		okay_empty = true;
	    else
		goto error;
	}

	/* now check the type */
	if(!okay_empty && VC_check_token_match(*list->car()->key(), type) <= 0) {
	    if(!(accept_ast && strcmp(*list->car()->key(), "-*-") == 0))
		goto error;
	}

	if (c == 'S' && list->car()->key()->type())
	    goto error;

	Estring** dptr = va_arg(args, Estring**);

	if (dptr) {
	    if (okay_empty)
		(*dptr) = NULL;
	    else
		(*dptr) = list->car()->key();
	}

	list = list->cdr();
	len -= 1;
    }

    va_end(args);
    return len == 0;
error:
    va_end(args);
    return false;
}

/* Project read functions
 */

PrVoidError ProjectDescriptor::created_by_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LNNN", NULL, &_created_by_major,
		  &_created_by_minor, &_created_by_micro ))
	return bad_project_file(s, "badly formed Created-By-Prcs-Version entry");

    return NoError;
}

PrVoidError ProjectDescriptor::project_ver_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LLLN", NULL, &_project_version_name,
		  &_project_version_major, &_project_version_minor ))
	return bad_project_file(s, "badly formed Project-Version entry");

    _project_version_point = new Estring (this, s->end_index());

    return NoError;
}

PrVoidError ProjectDescriptor::parent_ver_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "*LLLN", NULL, &_parent_version_name,
		  &_parent_version_major, &_parent_version_minor )) {
	return bad_project_file(s, "badly formed Parent-Version entry");
    } else {
	bool major_absent = strcmp(*_parent_version_name, "-*-") == 0;
	bool minor_absent = strcmp(*_parent_version_major, "-*-") == 0;
	bool name_absent = strcmp(*_parent_version_minor, "-*-") == 0;

	if(major_absent ^ minor_absent ||
	   major_absent ^ name_absent ||
	   name_absent ^ minor_absent)
	    return bad_project_file(s, "illegal use of -*-");
    }

    return NoError;
}

PrVoidError ProjectDescriptor::project_desc_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LS", NULL, &_project_description))
	return bad_project_file(s, "badly formed Project-Description entry");

    return NoError;
}

PrVoidError ProjectDescriptor::version_log_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LS", NULL, &_version_log)) {
	return bad_project_file(s, "badly formed Version-Log entry");
    }

    return NoError;
}

PrVoidError ProjectDescriptor::new_version_log_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LS", NULL, &_new_version_log))
	return bad_project_file(s, "badly formed New-Version-Log entry");

    return NoError;
}

PrVoidError ProjectDescriptor::checkin_time_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LS", NULL, &_checkin_time)) {
	return bad_project_file(s, "badly formed Checkin-Time entry");
    }

    return NoError;
}

PrVoidError ProjectDescriptor::checkin_login_prj_entry_func(const Sexp* s)
{
    if(!sexp_scan(s, "LU", NULL, &_checkin_login))
	return bad_project_file(s, "badly formed Checkin-Login entry");

    return NoError;
}

PrVoidError ProjectDescriptor::descends_from_prj_entry_func(const Sexp* s)
{
    /* Backward compatibility, its just ignored. */
    _descends_from_marker = ListMarker (s->start_index(), s->end_index());

    _deleted_markers->append (&_descends_from_marker);

    return NoError;
}

PrVoidError ProjectDescriptor::populate_ignore_prj_entry_func(const Sexp* s)
{
    _populate_ignore_marker = ListMarker (s->start_index(), s->end_index());
    _populate_ignore_point = new Estring (this, s->end_index());

    if(s->length() != 2 || !s->cadr()->is_pair())
	return bad_project_file(s, "malformed Populate-Ignore entry");

    foreach_sexp (t_ptr, s->cadr()) {
	const Sexp *t = *t_ptr;

	if(t->is_pair() || t->key()->type() != EsStringLiteral)
	    return bad_project_file(t, "expected a string");
    }

    foreach_sexp (t_ptr, s->cadr()) {
	Dstring* ds = (*t_ptr)->key();
	const char* t = ds->cast();
	_populate_ignore->insert (t, t);
    }

    return NoError;
}

PrVoidError ProjectDescriptor::project_keywords_prj_entry_func (const Sexp* s)
{
    _project_keywords_marker = ListMarker (s->start_index(), s->end_index());
    _project_keywords_point = new Estring (this, s->end_index());

    foreach_sexp (t_ptr, s->cdr()) {
	const Sexp *t = *t_ptr;

	if(!t->is_pair() ||
	   t->length() != 2 ||
	   t->car()->is_pair() ||
	   t->cadr()->is_pair() ||
	   t->car()->key()->type() != EsNameLiteral)
	    return bad_project_file(t, "malformed Project-Keywords entry");

	if (is_builtin_keyword (t->car()->key()->cast(), t->car()->key()->length()))
	    return bad_project_file(t, "Project-Keywords may not contain a builtin keyword");

	if (_project_keywords->isdefined (t->car()->key()->cast()))
	    return bad_project_file(t, "duplicate keyword");

	if (strchr (t->cadr()->key()->cast(), '\n'))
	    return bad_project_file(t, "keyword value may not contain a `$' character");

	_project_keywords->insert (t->car()->key()->cast(), t->cadr()->key()->cast());
	_project_keywords_extra->insert (t->car()->key()->cast(), t->cadr()->key()->cast());
    }

    return NoError;
}

PrVoidError ProjectDescriptor::new_merge_parents_prj_entry_read (int offset)
{
    _new_merge_parents_marker.start = offset;

    Return_if_fail (read_merge_parents(_new_merge_parents, &_new_merge_parents_marker));

    return NoError;
}


PrVoidError ProjectDescriptor::merge_parents_prj_entry_read(int offset)
{
    _merge_parents_marker.start = offset;

    Return_if_fail (read_merge_parents(_merge_parents, &_merge_parents_marker));

    return NoError;
}

PrVoidError ProjectDescriptor::files_prj_entry_read(int /*offset*/)
{
    const Sexp* s;
    DstringPtrArray gtags;
    bool allow_tags = true;

    Return_if_fail (s << read_list_elt());

    while (!s->is_empty()) {

	if (s->is_pair()) {
	    Return_if_fail(individual_insert_fileent(s, &gtags));
	    allow_tags = false;
	} else if (allow_tags && s->key()->index(0) == ':') {
	    gtags.append (s->key());
	} else {
	    return bad_project_file(s, "unexpected or illegal file attribute");
	}

	delete s;

	Return_if_fail(s << read_list_elt());
    }

    /* @@@ */
    _files_insertion_point = new Estring(this, prj_lex_this_index);

    Return_if_fail(verify_project_file());

    return NoError;
}

/* Read Helpers.
 */

static ListMarker *finish_me = NULL;

PrjTokenType prj_lex()
{
    int val = prjlex();

    if (finish_me) {
	finish_me->stop = prj_lex_this_index + (val == PrjEof);
	finish_me = NULL;
    }

    return (PrjTokenType) val;
}

PrVoidError ProjectDescriptor::read_merge_parents (MergeParentEntryPtrArray* array,
						   ListMarker* finish)
{
    PrjTokenType tok;

    while (true) {
	tok = prj_lex();

	switch (tok) {
	case PrjNull:
	case PrjBadString:
	case PrjEof:
	case PrjString:
	case PrjName:
	    return read_parse_bad_token(tok);

	case PrjClose:
	    finish_me = finish;

	    return NoError;
	case PrjOpen:
	    Return_if_fail (read_merge_parents_entry (array));
	}
    }

    return NoError;
}

PrVoidError ProjectDescriptor::read_merge_parents_file_name (const Sexp* s, Estring** fill)
{
    if (s->is_pair() && s->length() == 0)
	*fill = NULL;
    else if (!s->is_pair())
	*fill = s->key();
    else
	return bad_project_file (s, "bad merge parents entry");

    return NoError;
}

PrVoidError ProjectDescriptor::read_merge_parents_entry (MergeParentEntryPtrArray* array)
{
    const Sexp* s;
    MergeParentState state = MergeStateParent;
    MergeParentEntry* entry = NULL;
    MergeParentFile* file;
    Dstring* selected_version = NULL;
    Estring* working = NULL;
    Estring* common = NULL;
    Estring* selected = NULL;
    MergeAction action;

    Return_if_fail (s << read_list_elt());

    if (s->is_pair() || !strchr (s->key()->cast(), '.'))
	return bad_project_file (s, "expected version number");

    selected_version = s->key();

    delete s;
    Return_if_fail (s << read_list_elt());

    /* So, if we encounter 3 elements, its the stupid 1.1 format, 1
     * element, its the stupid 1.0 and earlier format, 2 elements and
     * its the (hopefully less stupid) current format. */

    if (!s->is_pair()) {
	/* 1.1 or 1.2 format. */
	const Sexp* elt_2 = s;
	const Sexp* elt_3;

	Return_if_fail (elt_3 << read_list_elt());

	if (elt_3->is_pair()) {
	    /* 1.2 format. */

	    if (strcmp (elt_2->key()->cast(), "complete") == 0) {
		state = MergeStateParent;
	    } else if (strcmp (elt_2->key()->cast(), "incomplete-parent") == 0) {
		state = MergeStateIncompleteParent;
	    } else if (strcmp (elt_2->key()->cast(), "incomplete") == 0) {
		state = MergeStateIncomplete;
	    } else {
		return bad_project_file (elt_2, "expected completion state");
	    }

	    delete elt_2;
	    s = elt_3;
	} else {
	    /* 1.1 format. */

	    if (strcmp (elt_3->key()->cast(), "complete") == 0) {
		state = MergeStateParent;
	    } else if (strcmp (elt_3->key()->cast(), "incomplete") == 0) {
		state = MergeStateIncomplete;
	    } else {
		return bad_project_file (elt_3, "expected completion state");
	    }

	    delete elt_2;
	    delete elt_3;
	    Return_if_fail (s << read_list_elt());
	}
    }

    if (array) {
	entry = new MergeParentEntry (*selected_version,
				      state,
				      prj_lineno);
	array->append (entry);
    }

    while (!s->is_empty()) {
	if (!s->is_pair() || s->length() != 4)
	    return bad_project_file (s, "bad merge parents entry");

	Return_if_fail (read_merge_parents_file_name (s->car(), &working));
	Return_if_fail (read_merge_parents_file_name (s->cadr(), &common));
	Return_if_fail (read_merge_parents_file_name (s->caddr(), &selected));

	if (s->cadddr()->is_pair() || s->cadddr()->key()->length() != 1)
	    return bad_project_file (s, "bad merge parents entry");

	switch (s->cadddr()->key()->index(0)) {
	case 'a': case 'd': case 'n': case 'm': case 'r':
	    action = (MergeAction) s->cadddr()->key()->index(0);
	    break;
	default:
	    return bad_project_file (s, "bad merge parents entry");
	}

	if (entry) {
	    file = new MergeParentFile (working ? working->cast() : NULL,
					common ? common->cast() : NULL,
					selected ? selected->cast() : NULL,
					action);

	    entry->all_files.append (file);
	}

	delete s;
	Return_if_fail (s << read_list_elt());
    }

    return NoError;
}

PrVoidError ProjectDescriptor::individual_insert_fileent(const Sexp* file, const DstringPtrArray* gtags)
{
    Dstring *working_path    = NULL;
    Dstring *desc_name       = NULL;
    Dstring *version_num     = NULL;
    Dstring *link_name       = NULL;
    const Sexp *file_descriptor = NULL;
    mode_t   file_mode       = 0644;
    ListMarker ent_marker;
    DstringPtrArray tags;
    const PrcsAttrs* attrs;

    if(!file->is_pair())
	return bad_project_file(file, "expected a list");

    if(file->length() < 2)
	return bad_project_file(file, "file entry too short");

    if(file->car()->is_pair())
	return bad_project_file(file, "expected a filename");

    if(!file->cadr()->is_pair())
	return bad_project_file(file, "malformed internal file identifier");

    if(weird_pathname(*file->car()->key()))
	return bad_project_file(file, "illegal pathname");

    if (project_path()[0]) {
	/* This saves allocating a new string if its going to be the
	 * same as the one in the project file. */
	working_path = new Dstring(project_path());

	working_path->append(*file->car()->key());
    } else {
	working_path = file->car()->key();
    }

    ent_marker = file->marker();
    file_descriptor = file->cadr();

    foreach_sexp (elt_ptr, file_descriptor) {
	if ((*elt_ptr)->is_pair())
	    return bad_project_file (*elt_ptr, "malformed internal file descriptor");
    }

    tags.append (*gtags);
    foreach_sexp (tag_ptr, file->cdr()->cdr()) {
	const Sexp* tag = *tag_ptr;

	if(tag->is_pair() || tag->key()->index(0) != ':')
	    return bad_project_file(file, "invalid file attribute");

	tags.append ((*tag_ptr)->key());
    }

    Return_if_fail (attrs << intern_attrs (&tags, gtags->length(), working_path->cast(), true));

    if ( attrs->type() == Directory || attrs->type() == SymLink ) {
	if (attrs->type() == Directory) {
	    if (file_descriptor->length() > 0)
		return bad_project_file(file, "malformed internal file descriptor");
	} else {
	    if (file_descriptor->length() > 1)
		return bad_project_file(file, "malformed internal file descriptor");

	    if (file_descriptor->length() == 1)
		link_name = file_descriptor->car()->key();
	}
    } else {
	if(file_descriptor->length() == 0) {
	    /* its empty */
	} else {
	    if(file_descriptor->length() == 2) {
		/* its the old format -- no file mode */
	    } else if(file_descriptor->length() == 3) {

		const char *mode_str = *file_descriptor->cdr()->cadr()->key();

		file_mode = 0;

		if(strlen(mode_str) != 3)
		    return bad_project_file(file, "invalid file mode");

		while(*mode_str) {
		    char c = *mode_str++;

		    if (c < '8' && c >= '0') {
			file_mode += c - '0';
			if(*mode_str)
			    file_mode <<= 3;
		    } else {
			return bad_project_file(file, "invalid file mode");
		    }
		}

	    } else {
		return bad_project_file(file, "malformed internal file descriptor");
	    }

	    version_num = file_descriptor->cadr()->key();

	    if (strncmp (*version_num, "1.", 2) != 0)
		return bad_project_file (file, "malformed internal file descriptor");

	    Estring *int_desc = file_descriptor->car()->key();

	    const char* int_desc_c  = *int_desc;
	    const char* first_slash = strchr(int_desc_c, '/');

	    if(!first_slash || strlen(first_slash) < 4) /* /0_a is legal (4 chars) */
		return bad_project_file(file, "malformed internal file descriptor");

	    int diff = (first_slash + 1) - int_desc_c;

	    desc_name = new Estring (this,
				     (char*)int_desc_c + diff,
				     int_desc->offset() + diff,
				     int_desc->offset_length() - diff,
				     EsNameLiteral);
	}
    }

    _file_entries->append(new FileEntry(working_path,
					ent_marker,
					attrs,
					version_num,
					desc_name,
					link_name,
					file_mode));

    return NoError;
}



PrVoidError ProjectDescriptor::verify_project_file()
{
    /* This code insures:
     * no descriptor matches the project file's.
     * no filename matches the project file's.
     * no descriptor matches another.
     * no filename matches another.
     * no filename is a prefix of another unless it has :directory
     */

    Dstring project_descriptor;

    project_descriptor.sprintf("%s.prj", project_name());

    /* Using the project_file_path and project_file_aux will
     * be wrong later */
    Dstring project_file_name(project_file_path());
    Dstring project_aux_name(project_aux_path());

    PathTable descriptors(pathname_hash, pathname_equal),
	      filenames  (pathname_hash, pathname_equal);

    descriptors.insert(project_descriptor.cast(), RealFile);
    filenames.insert(project_file_name.cast(), RealFile);
    filenames.insert(project_aux_name.cast(), RealFile);

    foreach_fileent(fe_ptr, this) {
	FileEntry* fe = *fe_ptr;

	const char* fe_name = fe->working_path();

	if(filenames.isdefined(fe_name)) {
	    if(strcmp(fe_name, project_file_name) == 0)
		pthrow prcserror << "Entry " << squote(fe_name)
				<< " duplicates project file name" << dotendl;
	    else if (strcmp(fe_name, project_aux_name) == 0)
		pthrow prcserror << "Entry " << squote(fe_name)
				<< " duplicates project auxiliary file name" << dotendl;
	    else
		pthrow prcserror << "Entry " << squote(fe_name)
				<< " is a duplicate file name" << dotendl;
	}

	filenames.insert(fe_name, fe->file_type());

	if(fe->file_type() != RealFile || !fe->descriptor_name())
	    continue;

	const char* fe_desc = fe->descriptor_name();

	if(descriptors.isdefined(fe_desc)) {
	    if(strcmp(fe_desc, project_descriptor) == 0)
		pthrow prcserror << "Entry " << squote(fe_name)
				<< " duplicates project file descriptor" << dotendl;
	    else
		pthrow prcserror << "Entry " << squote(fe_name)
				<< " contains a duplicate file descriptor" << dotendl;
	}

	descriptors.insert(fe_desc, RealFile);
    }

    foreach_fileent(fe_ptr, this) {
	FileEntry* fe = *fe_ptr;

	Dstring name(fe->working_path());

	for(int i = name.length() - 1; i > 0; i -= 1) {
	    if (name.index(i) == '/') {
		name.truncate(i);

		FileType* ft = filenames.lookup(name);

		if (ft && (*ft) != Directory)
		    pthrow prcserror << "Non-directory filename " << squote(name)
				    << " is prefix to another file" << dotendl;
	    }
	}
    }

    return NoError;
}

/* merge_notify, has_been_merged --
 *
 *     these two functions deserve documentation.  merge_notify is called to
 *     update the project file immediately after a merge action is taken, in
 *     merge_finish.  the MergeAction argument identifies the type of action
 *     taken.  the goal is to save all information and avoid offering to merge
 *     a particular working file twice.  The data saved is as follows:
 *
 *     (working_name common_name selected_name action)
 *
 *     file families are not saved because for common and selected versions
 *     they are well defined.  for working files, they are not guaranteed to
 *     exist.  both names and families may change with merges and users edits
 *     so tracking merges by them is not useful, in my opinion.
 */
void ProjectDescriptor::merge_notify(FileEntry* working,
				     FileEntry* common,
				     FileEntry* selected,
				     MergeAction ma) const
{
    MergeParentFile* new_entry;

    new_entry = new MergeParentFile (working ? working->working_path() + cmd_root_project_path_len : NULL,
				     common ? common->working_path() + cmd_root_project_path_len : NULL,
				     selected ? selected->working_path() + cmd_root_project_path_len : NULL,
				     ma);

    _merge_entry->all_files.append (new_entry);
}

/* algorithm: to be true,
 * for each of w,c,s, if a lookup of filename in the correct
 * table must succeed.
 */
bool ProjectDescriptor::has_been_merged(FileEntry* working,
					FileEntry* common,
					FileEntry* selected) const
{
    return
	(working  && _merge_entry->working_table ->isdefined(working ->working_path() + cmd_root_project_path_len)) ||
	(common   && _merge_entry->common_table  ->isdefined(common  ->working_path() + cmd_root_project_path_len)) ||
	(selected && _merge_entry->selected_table->isdefined(selected->working_path() + cmd_root_project_path_len));
}

bool ProjectDescriptor::merge_continuing (ProjectVersionData *selected)
{
    _merge_selected_major = selected->prcs_major();
    _merge_selected_minor = selected->prcs_minor();

    Dstring sel_version_name;
    bool return_val = false;

    sel_version_name.append (selected->prcs_major());
    sel_version_name.append ('.');
    sel_version_name.append (selected->prcs_minor());

    _deleted_markers->append (&_merge_parents_marker);
    _deleted_markers->append (&_new_merge_parents_marker);

    _append_merge_parents = true;

    _merge_entry = new MergeParentEntry (sel_version_name, MergeStateIncomplete, 0);
    _new_merge_parents->append (_merge_entry);

    _merge_entry->working_table  = new MergeParentTable;
    _merge_entry->common_table   = new MergeParentTable;
    _merge_entry->selected_table = new MergeParentTable;

    foreach (ent_ptr, _new_merge_parents, MergeParentEntryPtrArray::ArrayIterator) {
	MergeParentEntry *entry = *ent_ptr;

	if (strcmp (entry->selected_version, sel_version_name) != 0)
	    continue;

	foreach (file_ptr, &entry->all_files, MergeParentFilePtrArray::ArrayIterator) {
	    MergeParentFile *file = *file_ptr;

	    return_val = true;

	    if (file->working)  _merge_entry->working_table ->insert (file->working , file);
	    if (file->common)   _merge_entry->common_table  ->insert (file->common  , file);
	    if (file->selected) _merge_entry->selected_table->insert (file->selected, file);
	}
    }

    return return_val;
}

void ProjectDescriptor::merge_finish()
{
    if(!_merge_complete || _merge_incomplete) {
	prcsinfo << "Merge incomplete" << dotendl;
	merge_log() << prcsendl << "*** Merge incomplete" << prcsendl << prcsendl;
    } else {
	_merge_entry->state = MergeStateParent;

	prcsinfo << "Merge against version "
		 << _merge_selected_major << "."
		 << _merge_selected_minor << " complete" << dotendl;

	merge_log() << prcsendl << "*** Merge complete" << prcsendl << prcsendl;
    }
}

void ProjectDescriptor::delete_merge_parents (MergeParentEntryPtrArray* array)
{
    if (array) {
	foreach (ent_ptr, array, MergeParentEntryPtrArray::ArrayIterator) {
	    MergeParentEntry *entry = *ent_ptr;

	    if (entry->working_table) delete entry->working_table;
	    if (entry->common_table) delete entry->common_table;
	    if (entry->selected_table) delete entry->selected_table;

	    foreach (file_ptr, &entry->all_files, MergeParentFilePtrArray::ArrayIterator)
		delete (*file_ptr);
	}
    }
}

PrProjectVersionDataPtrError ProjectDescriptor::check_version_name(const char* name, int line) const
{
    ProjectVersionData* data;

    const char* this_version_major = major_version_of(name);
    const char* this_version_minor = minor_version_of(name);

    if(!this_version_major)
	pthrow prcserror << fileline(_prj_source_name, line)
			<< "Illegal version number " << name << dotendl;

    if(VC_check_token_match(this_version_major, "label") <= 0 ||
       VC_check_token_match(this_version_minor, "number") <= 0)
	pthrow prcserror << fileline(_prj_source_name, line)
			<< "Illegal version number " << name << dotendl;

    data = repository_entry()->lookup_version(this_version_major, this_version_minor);

    if (!data)
	pthrow prcserror << fileline(_prj_source_name, line)
			<< "Version " << name
			<< " not found in repository" << dotendl;

    return data;
}

PrVoidError ProjectDescriptor::adjust_merge_parents()
{
    _deleted_markers->append (&_merge_parents_marker);
    _deleted_markers->append (&_new_merge_parents_marker);

    _append_merge_parents = true;

    Return_if_fail(verify_merge_parents());

    delete_merge_parents (_merge_parents);

    _merge_parents = _new_merge_parents;

    _new_merge_parents = NULL;

    return NoError;
}

/* Keywords
 */

void ProjectDescriptor::add_keyword (const char* key, const char* val)
{
    _alter_popkey = true;

    ASSERT (! _project_keywords_extra->lookup (key), "blah");

    _project_keywords->insert (key, val);
    _project_keywords_extra->insert (key, val);
}

void ProjectDescriptor::rem_keyword (const char* key)
{
    _alter_popkey = true;

    ASSERT (_project_keywords_extra->lookup (key), "blah");

    _project_keywords->remove (key);
    _project_keywords_extra->remove (key);
}

void ProjectDescriptor::set_keyword (const char* key, const char* val)
{
    _alter_popkey = true;

    ASSERT (_project_keywords_extra->lookup (key), "blah");

    _project_keywords->insert (key, val);
    _project_keywords_extra->insert (key, val);
}

void ProjectDescriptor::add_populate_ignore (const char* key)
{
    _alter_popkey = true;

    ASSERT (! _populate_ignore->lookup (key), "blah");

    _populate_ignore->insert (key, key);
}

void ProjectDescriptor::rem_populate_ignore (const char* key)
{
    _alter_popkey = true;

    ASSERT (_populate_ignore->lookup (key), "blah");

    _populate_ignore->remove (key);
}

OrderedStringTable* ProjectDescriptor::project_keywords_extra() const
{
    return _project_keywords_extra;
}

KeywordTable* ProjectDescriptor::project_keywords(FileEntry* fe, bool setting)
{
    if (!_keyword_id) {
	_keyword_id = new Dstring;
	_keyword_pversion = new Dstring;
	_keyword_pheader = new Dstring;
    } else if (!setting) {
	/* Values don't matter, just the keywords. */
	return _project_keywords;
    }

    if (setting) {
	ASSERT(fe->last_mod_date() != NULL &&
	       fe->last_mod_user() != NULL, "missing entries in file entry");

	_keyword_id->sprintf("%s %s %s %s",
			     strip_leading_path(fe->working_path()),
			     fe->descriptor_version_number(),
			     fe->last_mod_date(),
			     fe->last_mod_user());

	_project_keywords->insert("Id", _keyword_id->cast());
	_project_keywords->insert("Author", fe->last_mod_user());
	_project_keywords->insert("Date", fe->last_mod_date());
	_project_keywords->insert("Basename", strip_leading_path(fe->working_path()));
	_project_keywords->insert("Revision", fe->descriptor_version_number());
	_project_keywords->insert("Source", fe->working_path());
    } else {
	_project_keywords->insert("Id", "");
	_project_keywords->insert("Author", "");
	_project_keywords->insert("Date", "");
	_project_keywords->insert("Basename", "");
	_project_keywords->insert("Revision", "");
	_project_keywords->insert("Source", "");
    }

    if (_keyword_pversion->length() == 0) {
	_keyword_pversion->sprintf("%s.%s",
				   project_version_major()->cast(),
				   project_version_minor()->cast());

	_keyword_pheader->sprintf("%s %s %s %s",
				  project_name(),
				  _keyword_pversion->cast(),
				  checkin_time()->cast(),
				  checkin_login()->cast());

	_project_keywords->insert("Project", project_name());
	_project_keywords->insert("ProjectDate", checkin_time()->cast());
	_project_keywords->insert("ProjectAuthor", checkin_login()->cast());
	_project_keywords->insert("ProjectMajorVersion", project_version_major()->cast());
	_project_keywords->insert("ProjectMinorVersion", project_version_minor()->cast());
	_project_keywords->insert("ProjectVersion", _keyword_pversion->cast());
	_project_keywords->insert("ProjectHeader", _keyword_pheader->cast());
    }

    return _project_keywords;
}


/* File Insertion
 */

void ProjectDescriptor::append_files_data (const char* data)
{
    _files_insertion_point->append (data);
}

void ProjectDescriptor::append_file (const char* working_path,
				     bool keywords)
{
    _files_insertion_point->append ("\n  (");
    _files_insertion_point->append_protected (working_path);
    _files_insertion_point->append (" ()");

    if (keywords)
	_files_insertion_point->append (" :no-keywords");

    _files_insertion_point->append (")");
}

void ProjectDescriptor::append_link (const char* working_path,
				     const char* link_name)
{
    _files_insertion_point->append ("\n  (");
    _files_insertion_point->append_protected (working_path);
    _files_insertion_point->append (" (");
    _files_insertion_point->append_protected (link_name);
    _files_insertion_point->append (") :symlink)");
}

void ProjectDescriptor::append_directory (const char* working_path)
{
    _files_insertion_point->append ("\n  (");
    _files_insertion_point->append_protected (working_path);
    _files_insertion_point->append (" () :directory)");
}

void ProjectDescriptor::append_file_deletion (FileEntry* fe)
{
    _files_insertion_point->append ("\n  ; (");
    _files_insertion_point->append_protected (fe->working_path());
    _files_insertion_point->append (" ()");

    fe->file_attrs()->print_to_string (_files_insertion_point, true);

    _files_insertion_point->append (")");
}

void ProjectDescriptor::append_file (FileEntry* fe)
{
    _files_insertion_point->append ("\n  (");
    _files_insertion_point->append_protected (fe->working_path());
    if (fe->file_type() == RealFile && fe->descriptor_name()) {
	_files_insertion_point->append (" (");
	_files_insertion_point->append_protected (project_name());
	_files_insertion_point->append ("/");
	_files_insertion_point->append_protected (fe->descriptor_name());
	_files_insertion_point->append (" ");
	_files_insertion_point->append_protected (fe->descriptor_version_number());
	_files_insertion_point->sprintfa (" %03o)", fe->file_mode());
    } else {
	_files_insertion_point->append (" ()");
    }

    fe->file_attrs()->print_to_string (_files_insertion_point, true);

    _files_insertion_point->append (")");
}

/* Attributes */

PrcsAttrs::PrcsAttrs (const DstringPtrArray *vals0, int ngroup)
    :_vals(*vals0),
     _ngroup(ngroup),
     _type(RealFile),
     _project(NULL),
     _keyword_sub(true)
{
    memcpy (_merge_actions, default_merge_actions, sizeof (_merge_actions));
    memcpy (_merge_descs,   default_merge_descs  , sizeof (_merge_descs));

    const char* env;

    env = get_environ_var ("PRCS_MERGE_COMMAND");

    if (env)
	_mergetool.assign (env);

    env = get_environ_var ("PRCS_DIFF_COMMAND");

    if (env)
	_difftool.assign (env);
}

FileType PrcsAttrs::type() const { return _type; }
bool PrcsAttrs::keyword_sub() const { return _keyword_sub; }
ProjectDescriptor* PrcsAttrs::project() const { return _project; };
MergeAction PrcsAttrs::merge_action (int rule) const { return _merge_actions[rule]; };
const char* PrcsAttrs::merge_desc (int rule) const { return _merge_descs[rule]; };
const char* PrcsAttrs::merge_tool () const { return _mergetool.length() > 0 ? _mergetool.cast() : NULL; }
const char* PrcsAttrs::diff_tool () const { return _difftool.length() > 0 ? _difftool.cast() : NULL; }

void PrcsAttrs::print_to_string (Dstring* str, bool lead_if) const
{
    if (lead_if && _vals.length() > _ngroup)
	str->append (' ');

    for (int i = _ngroup; i < _vals.length(); i += 1) {
	str->append (_vals.index(i)->cast());

	if (i < (_vals.length() - 1))
	    str->append (' ');
    }
}

void PrcsAttrs::print (ostream& os, bool lead_if) const
{
    if (lead_if && _vals.length() > _ngroup)
	os << ' ';

    for (int i = _ngroup; i < _vals.length(); i += 1) {
	os << _vals.index(i);

	if (i < (_vals.length() - 1))
	    os << ' ';
    }
}

bool PrcsAttrs::regex_matches (reg2ex2_t *re) const
{
    foreach (ds_ptr, &_vals, DstringPtrArray::ArrayIterator) {
	if (prcs_regex_matches ((*ds_ptr)->cast(), re))
	    return true;
    }

    return false;
}

PrVoidError ProjectDescriptor::validate_attrs (const PrcsAttrs *attrs, const char* name)
{
    bool seen_type = false;

    foreach (ds_ptr, &attrs->_vals, DstringPtrArray::ArrayIterator) {
	Dstring an = (*ds_ptr)->cast();
	Dstring av;

	const char* equal = strchr (an.cast(), '=');

	if (equal) {
	    av.assign (equal + 1);
	    an.truncate (equal - an.cast());
	}

	const AttrDesc* ad = is_file_attribute (an.cast(), an.length());

	if (!ad)
	    pthrow prcserror  << "Unrecognized attribute " << an << " for file " << squote(name) << dotendl;

	switch (ad->type) {
	case RealFileAttr:
	case DirectoryAttr:
	case SymlinkAttr:
	    if (seen_type)
		pthrow prcserror << "Duplicate file type attribute for file " << squote(name) << dotendl;
	    seen_type = true;
	    break;
	case Mergerule1Attr:
	case Mergerule2Attr:
	case Mergerule3Attr:
	case Mergerule4Attr:
	case Mergerule5Attr:
	case Mergerule6Attr:
	case Mergerule7Attr:
	case Mergerule8Attr:
	case Mergerule9Attr:
	case Mergerule10Attr:
	case Mergerule11Attr:
	case Mergerule12Attr:
	case Mergerule13Attr:
	case Mergerule14Attr:
	    if (av.length() != 1 || strchr("andrm", av.index(0)) == NULL)
		pthrow prcserror << "Illegal merge action " << av << dotendl;
	    break;
	case MergetoolAttr:
	case DifftoolAttr:
	    if (av.length() < 1)
		pthrow prcserror << "Illegal tool name " << av << dotendl;
	default:
	    break;
	}
    }

    return NoError;
}

void ProjectDescriptor::init_attrs (PrcsAttrs *attrs)
{
    attrs->_project = this;

    foreach (ds_ptr, &attrs->_vals, DstringPtrArray::ArrayIterator) {
	Dstring an = (*ds_ptr)->cast();
	Dstring av;

	const char* equal = strchr (an.cast(), '=');

	if (equal) {
	    av.assign (equal + 1);
	    an.truncate (equal - an.cast());
	}

	const AttrDesc* ad = is_file_attribute (an.cast(), an.length());
	int mergerule = 0;

	switch (ad->type) {
	case RealFileAttr:
	case ProjectFileAttr:
	    attrs->_type = RealFile;
	    break;
	case DirectoryAttr:
	case ImplicitDirectoryAttr:
	    attrs->_type = Directory;
	    break;
	case SymlinkAttr:
	    attrs->_type = SymLink;
	    break;
	case NoKeywordAttr:
	    attrs->_keyword_sub = false;
	    break;
	case MergetoolAttr:
	    attrs->_mergetool.assign (av);
	    break;
	case DifftoolAttr:
	    attrs->_difftool.assign (av);
	    break;
	case TagAttr:
	    break;
	case Mergerule14Attr: mergerule += 1;
	case Mergerule13Attr: mergerule += 1;
	case Mergerule12Attr: mergerule += 1;
	case Mergerule11Attr: mergerule += 1;
	case Mergerule10Attr: mergerule += 1;
	case Mergerule9Attr: mergerule += 1;
	case Mergerule8Attr: mergerule += 1;
	case Mergerule7Attr: mergerule += 1;
	case Mergerule6Attr: mergerule += 1;
	case Mergerule5Attr: mergerule += 1;
	case Mergerule4Attr: mergerule += 1;
	case Mergerule3Attr: mergerule += 1;
	case Mergerule2Attr: mergerule += 1;
	case Mergerule1Attr: /* notice zero origin */
	    attrs->_merge_descs[mergerule] = "User supplied action, no help available";
	    attrs->_merge_actions[mergerule] = (MergeAction)av.index(0);
	    break;
	}
    }
}

PrPrcsAttrsPtrError ProjectDescriptor::intern_attrs (const DstringPtrArray* tagvals,
						     int ngroup,
						     const char* name,
						     bool validate)
{
    PrcsAttrs *it = new PrcsAttrs (tagvals, ngroup);
    PrcsAttrs **lu;

    lu = _attrs_table->lookup (it);

    if (lu) {
	delete it;
	return *lu;
    } else {
	if (validate)
	    Return_if_fail (validate_attrs (it, name));

	init_attrs (it);

	_attrs_table->insert (it, it);

	return it;
    }
}

bool attrs_equal(const PrcsAttrs*const& x, const PrcsAttrs*const& y)
{
    if (x->_ngroup != y->_ngroup || x->_vals.length() != y->_vals.length())
	return false;

    for (int i = 0; i < x->_vals.length(); i += 1) {
	if (strcmp (x->_vals.index(i)->cast(), y->_vals.index(i)->cast()) != 0)
	    return false;
    }

    return true;
}

extern int hash(const char *const& s, int M);

int attrs_hash(const PrcsAttrs*const & s, int M)
{
    int h = 0;
    const char* p;

    for (int i = 0; i < s->_vals.length(); i += 1) {
	p = s->_vals.index(i)->cast();
	h += hash (p, M);
    }

    return h % M;
}


/* Updates and Deletions.
 */

PrVoidError ProjectDescriptor::write_project_file(const char* filename)
{
    if (strcmp (*project_version_name(), project_name()) != 0) {

	_project_version_point->sprintfa ("\n;; PRCS note: this project version was "
					  "originally checked in when\n;; the project "
					  "was named `%s'.\n",
					  project_version_name()->cast());

	project_version_name()->assign(project_name());
    }

    project_version_name()->assign(project_name());

    if (strcmp(parent_version_minor()->cast(), "-*-") != 0)
	parent_version_name()->assign(project_name());

    created_by_major()->assign_int(prcs_version_number[0]);
    created_by_minor()->assign_int(prcs_version_number[1]);
    created_by_micro()->assign_int(prcs_version_number[2]);

    if (_alter_popkey) {
	_deleted_markers->append (& _populate_ignore_marker);
	_deleted_markers->append (& _project_keywords_marker);

	int first = 0;

	_project_keywords_point->append ("(Project-Keywords");
	_populate_ignore_point->append  ("(Populate-Ignore (");

	int ks = _project_keywords_extra->key_array()->length();

	for (int i = 0; i < ks; i += 1) {
	    int nspaces = (first * strlen ("(Project-Keywords")) + 1;

	    for (int j = 0; j < nspaces; j += 1)
		_project_keywords_point->append (" ");

	    _project_keywords_point->append ("(");
	    _project_keywords_point->append_protected (_project_keywords_extra->key_array()->index(i));
	    _project_keywords_point->append (" ");
	    _project_keywords_point->append_string (_project_keywords_extra->data_array()->index(i));
	    _project_keywords_point->append (")");

	    if (i < (ks - 1))
		_project_keywords_point->append ("\n");

	    first = 1;
	}

	first = 0;

	ks = _populate_ignore->key_array()->length();

	for (int i = 0; i < ks; i += 1) {
	    int nspaces = (first * strlen ("(PopulateIgnore ( "));

	    for (int j = 0; j < nspaces; j += 1)
		_populate_ignore_point->append (" ");

	    _populate_ignore_point->append_string (_populate_ignore->key_array()->index(i));

	    if (i < (ks - 1))
		_populate_ignore_point->append ("\n");

	    first = 1;
	}

	_project_keywords_point->append (")");
	_populate_ignore_point->append ("))");
    }

    Return_if_fail(make_subdirectories(filename));

    WriteableFile outfile;

    Return_if_fail (outfile.open(filename));

    really_write_project_file (outfile.stream());

    Return_if_fail (outfile.close());

    if (_read_mode != 0) {
	If_fail (Err_chmod (filename, _read_mode))
	    prcswarning << "Could not reset permissions on file " << squote (filename) << dotendl;
    }

    return NoError;
}

static int sort_marker (const void* a, const void* b)
{
    return (*((const ListMarker**)a ))->start -
	   (*((const ListMarker**)b ))->start;
}

void ProjectDescriptor::really_write_project_file (ostream& os)
{
    int buffer_index   = 0;
    int buffer_length  = _segment->length() - 1 /* the null */;

    int file_number    = 0;
    int marker_number  = 0;
    int estring_number = 0;

    int file_number_max = _file_entries->length();
    int marker_number_max = 0;
    int estring_number_max = _all_estrings->length();

    if (_deleted_markers) {
	marker_number_max = _deleted_markers->length();

	/* File entries and Estrings are in sorted order, they are read that way. */
	/* Markers must be sorted. */
	_deleted_markers->sort(sort_marker);
    }

    int next_file_index;
    int next_marker_index;
    int next_estring_index;
    int next_index;

    while (buffer_index <= buffer_length) {
	/* markers are never skipped, so get the next index. */
	if (marker_number < marker_number_max)
	    next_marker_index = _deleted_markers->index(marker_number)->start;
	else
	    next_marker_index = buffer_length + 1;

	/* estrings and files may be skipped by markers, so if the index
	 * found is less than the buffer_index, then its been skipped and
	 * we incr their number until one ahead of the point is found. */
	do {
	    if (file_number < file_number_max)
		next_file_index = _file_entries->index(file_number)->ent_marker()->start;
	    else
		next_file_index = buffer_length + 1;
	} while (next_file_index < buffer_index && (file_number += 1, true));

	do {
	    if (estring_number < estring_number_max)
		next_estring_index = ((Estring*)_all_estrings->index(estring_number))->offset();
	    else
		next_estring_index = buffer_length + 1;
	} while (next_estring_index < buffer_index && (estring_number += 1, true));

	next_index = next_estring_index < next_file_index   ? next_estring_index : next_file_index;
	next_index = next_index         < next_marker_index ? next_index         : next_marker_index;

	ASSERT (buffer_index <= next_index, "no way man!");

	if (next_index > buffer_length)
	    break;

	if (buffer_index < next_index) {
	    os.write (_segment->segment() + buffer_index, next_index - buffer_index );
	    buffer_index = next_index;
	} else if (next_index == next_marker_index) {
	    const ListMarker* marker = _deleted_markers->index(marker_number++);

	    buffer_index = marker->stop;
	} else if (next_index == next_estring_index) {
	    Estring* string = (Estring*) _all_estrings->index(estring_number);
	    char missing_char = _nullified_chars->index(estring_number++);

	    switch (string->type()) {
	    case EsStringLiteral:

		for (int i = 0; i < string->length(); i += 1) {
		    char c = string->index(i);

		    if (c == '\"' || c == '\\')
			os << '\\';

		    os << c;
		}

		ASSERT (missing_char == '\"', "must be");

		os << missing_char;

		buffer_index = string->offset() + string->offset_length() + 1;

		break;
	    case EsNameLiteral:
		print_protected (string->cast(), os);

		os << missing_char;

		buffer_index = string->offset() + string->offset_length() + 1;

		break;
	    case EsUnProtected:
		os << string;

		break;
	    }

	} else /* (next_index == next_file_index) */ {
	    FileEntry *fe = _file_entries->index (file_number++);

	    os << "(";
	    print_protected(fe->working_path(), os);
	    os << " (";

	    if (fe->file_type() == SymLink && fe->link_name()) {
		print_protected(fe->link_name(), os);
	    } else if (fe->file_type() == RealFile &&
		       fe->descriptor_name() &&
		       fe->descriptor_version_number()[0] != 0) {
		print_protected (project_name(), os);
		os << "/";
		print_protected (fe->descriptor_name(), os);

		char buf[8];
		sprintf(buf, "%03o", fe->file_mode());
		os << " " << fe->descriptor_version_number()
		   << " " << buf;
	    }

	    os << ")";

	    fe->file_attrs()->print (os, true);

	    os << ")";

	    buffer_index = fe->ent_marker()->stop;
	}
    }

    if (_append_merge_parents) {
	write_merge_parents ("Merge-Parents", _merge_parents, os);
	write_merge_parents ("New-Merge-Parents", _new_merge_parents, os);
    }
}

void ProjectDescriptor::write_merge_parents (const char* name,
					     MergeParentEntryPtrArray *entries,
					     ostream& os)
{
    os << "(" << name;

    if (entries) {
	if (entries->length() > 0)
	    os << "\n";

	for (int i = 0; i < entries->length(); i += 1) {
	    MergeParentEntry *entry = entries->index(i);

	    os << "  (" << entry->selected_version << " ";

	    switch (entry->state) {
	    case MergeStateIncomplete:
		os << "incomplete"; break;
	    case MergeStateParent:
		os << "complete"; break;
	    case MergeStateIncompleteParent:
		os << "incomplete-parent"; break;
	    }

	    int j = 0;

	    for (; j < entry->all_files.length(); j += 1) {
		MergeParentFile* file = entry->all_files.index(j);

		if (!(j % 2)) os << "\n    ";

		os << "(";
		if (file->working)
		    print_protected (file->working, os);
		else
		    os << "()";
		os << " ";
		if (file->common)
		    print_protected(file->common, os);
		else
		    os << "()";
		os << " ";
		if (file->selected)
		    print_protected(file->selected, os);
		else
		    os << "()";
		os << " " << (char)file->action << ")";

		if (!(j % 2)) os << " ";
	    }

	    if (j % 2) os << "\n  ";

	    os << ")\n";
	}
    }

    os << ")\n";
}

void ProjectDescriptor::delete_file (FileEntry* fe)
{
    _deleted_markers->append(fe->ent_marker());
}

/* Quick Elim
 */

void ProjectDescriptor::read_quick_elim()
{
    _quick_elim = NULL;

    If_fail(_quick_elim << ::read_quick_elim(project_aux_path()))
	return;
}

void ProjectDescriptor::quick_elim_update()
{
    if(!_quick_elim)
	return;

    _quick_elim->update_file(project_aux_path());
}

void ProjectDescriptor::quick_elim_update_fe(FileEntry* fe)
{
    if(!_quick_elim)
	_quick_elim = new QuickElim;

    _quick_elim->update(fe);
}

/* Match Fileent
 */
void ProjectDescriptor::populate_tables (void)
{
    _file_name_table = new FileTable(pathname_hash, pathname_equal);
    _file_desc_table = new FileTable(pathname_hash, pathname_equal);
    _file_match_cache = new EntryTable;

    foreach_fileent (fe_ptr, this) {
	FileEntry *fe = *fe_ptr;

	_file_name_table->insert (fe->working_path(), fe);

	if (fe->file_type() == RealFile && fe->descriptor_name())
	    _file_desc_table->insert (fe->descriptor_name(), fe);
    }
}

PrFileEntryPtrError ProjectDescriptor::match_fileent (FileEntry* fe)
{
    ProjectDescriptor *project = fe->project();

    /* fe is a FileEntry belonging to another project.  Try
     * to match it according to the standard rules (which are new
     * in 1.2.0b8). */

    if (!_file_name_table)
	populate_tables();

    FileEntry **cache_lu = _file_match_cache->lookup (fe);

    if (cache_lu)
	return *cache_lu;

    FileEntry **name_lu = _file_name_table->lookup(fe->working_path());

    if (fe->file_type() != RealFile || !fe->descriptor_name())
	return name_lu ? *name_lu : NULL;

    FileEntry **desc_lu = _file_desc_table->lookup(fe->descriptor_name());

    if (name_lu && desc_lu && *desc_lu != *name_lu) {
	prcsquery << "An attempt was made to match files between project version "
		  << full_version() << " and " << project->full_version()
		  << ".  The result is ambiguous, due to (perhaps pathological) "
	    "naming and renaming of files.  The file " << squote (fe->working_path())
		  << " in version " << full_version() << " matches by name with "
		  << squote ((*name_lu)->working_path()) << " and by file family with "
		  << squote ((*desc_lu)->working_path()) << " in version "
		  << project->full_version() << ".  You are advised to abort and prevent this "
		  << " from occuring, if possible.  You may continue by selecting "
	    "one of the matches, or abort.  "
		  << force ("Using the file family match")
		  << report ("Use the file family match")
		  << allow_bang (_match_bang)
		  << help (bad_match_help_string)
		  << option ('n', "Continue matching by name")
		  << defopt ('f', "Continue matching by file family")
		  << query ("Select");

	char c;

	Return_if_fail (c << prcsquery.result());

	if (c == 'n') {
	    _file_match_cache->insert (fe, *name_lu);
	    return *name_lu;
	} else {
	    _file_match_cache->insert (fe, *desc_lu);
	    return *desc_lu;
	}
    } else if (desc_lu) {
	return *desc_lu;
    } else if (name_lu) {
	return *name_lu;
    } else
	return (FileEntry*) 0;
}

bool ProjectDescriptor::can_add (FileEntry *fe)
{
    if (!_file_name_table)
	populate_tables();

    FileEntry **name_lu = _file_name_table->lookup(fe->working_path());

    if (name_lu)
	return false;

    if (fe->file_type() != RealFile || !fe->descriptor_name())
	return true;

    FileEntry **desc_lu = _file_desc_table->lookup(fe->descriptor_name());

    return desc_lu == NULL;
}

/* Estring
 */

Estring::Estring (ProjectDescriptor* project, char* s, size_t off, size_t len, EstringType type)
    :Dstring(), _offset(off), _len (len), _type(type)
{
    project->register_estring(this, s[len]);
    s[len] = 0;

    _filled = len;
    _vec = s;
}

Estring::Estring (ProjectDescriptor* project, size_t position)
    :Dstring(), _offset(position), _len (0), _type(EsUnProtected)
{
    project->register_estring(this, 0);
}

void Estring::modify()
{
    if (_alloc > 0 || _filled == 0)
	return;

    _alloc = 1;
    char* oldvec = _vec;

    while (_alloc < _filled + 1) _alloc <<= 1;

    _vec = NEWVEC0(char, _alloc);

    strcpy (_vec, oldvec);
}

void Estring::append_protected (const char* string)
{
    protect_path (string, this);
}

void Estring::append_string (const char* string)
{
    append ("\"");
    protect_string (string, this);
    append ("\"");
}

void ProjectDescriptor::register_estring(Estring* estring, char missing)
{
    if (!_all_estrings)
	_all_estrings = new DstringPtrArray;

    if (!_nullified_chars)
	_nullified_chars = new Dstring;

    _all_estrings->append (estring);
    _nullified_chars->append (missing);
}

bool Estring::is_dstring() { return false; }
EstringType Estring::type() const { return _type; };
size_t Estring::offset() const { return _offset; }
size_t Estring::offset_length() const { return _len; }

/* Parser
 */

#include "prj-names.h"

#define ENTRIES sizeof(ProjectDescriptor::_pftable)/sizeof(ProjectDescriptor::_pftable[0])

const int ProjectDescriptor::_prj_entries = ENTRIES;
bool ProjectDescriptor::_prj_entry_found[ENTRIES];

static MemorySegment *prj_seg;
static int prj_read_current_offset;

FILE* prj_input = NULL;

extern "C"
int prjinput(void* buf, int max_size)
{
    if (prj_input) {
	int c;

	If_fail (c << Err_fread( buf, max_size, prj_input))
	  return -1;

	return c;
    } else if (prj_seg) {
	int length = prj_seg->length() - 1;

	if (prj_read_current_offset + max_size > length)
	    max_size = length - prj_read_current_offset;

	memcpy (buf, prj_seg->segment() + prj_read_current_offset, max_size);

	prj_read_current_offset += max_size;

	return max_size;
    } else {
	return 0;
    }
}

PrVoidError ProjectDescriptor::parse_prj_file(FILE* infile)
{
    PrjTokenType tok;

    prj_seg = NULL;

    while (prj_lex() != PrjEof) { }

    _segment = prj_seg = new MemorySegment(true);

    prj_read_current_offset = 0;
    prj_lineno = 1;
    prj_lex_cur_index = 0;
    prj_lex_this_index = 0;

    Return_if_fail (prj_seg->map_file (_prj_source_name, fileno(infile)));

    _segment->append_segment("", 1); /* Added a null here for safety, going to
				      * have to get rid of it when writing. */

    memset(_prj_entry_found, 0, sizeof(_prj_entry_found));

    while (true) {
	tok = prj_lex();

	switch (tok) {
	case PrjNull:
	case PrjBadString:
	case PrjString:
	case PrjName:
	case PrjClose:
	    return read_parse_bad_token (tok);

	case PrjOpen:
	    Return_if_fail(read_list_checked(prj_lex_this_index));

	    break;
	case PrjEof:
	    for (int i = 0; i < _prj_entries; i += 1)
		if (_pftable[i].must_have && !_prj_entry_found[i])
		    pthrow prcserror << "Missing project file entry "
				    << squote(_pftable[i].name) << dotendl;

	    _end_of_buffer_point = new Estring (this, prj_lex_cur_index);

	    return NoError;
	}
    }
}

PrConstSexpPtrError ProjectDescriptor::read_list_elt()
{
    PrjTokenType tok = prj_lex();

    switch (tok) {
    case PrjEof:
    case PrjNull:
    case PrjBadString:
	return read_parse_bad_token (tok);
    case PrjString:
    case PrjName:
	return read_new_estring(tok);
    case PrjClose:
	return new Sexp (prj_lineno);
    case PrjOpen:
	break;
    }
    return read_list(prj_lex_this_index);
}

PrConstSexpPtrError ProjectDescriptor::read_new_estring(PrjTokenType type)
{
    int start_index = prj_lex_this_index + (type == PrjString);
    int stop_index = prj_lex_cur_index - (type == PrjString);

    Estring *estring = new Estring(this,
				   _segment->wr_segment() + start_index,
				   start_index,
				   stop_index - start_index,
				   type == PrjName ? EsNameLiteral : EsStringLiteral);

    if (strchr (estring->cast(), '\\')) {
	/* All names/strings are deslashified when read. */
	estring->truncate(0);
	correct_path(_segment->wr_segment() + start_index, estring);
    }

    return new Sexp (estring, prj_lineno);
}

PrVoidError ProjectDescriptor::read_parse_bad_token(PrjTokenType tok)
{
    switch (tok) {
    case PrjEof:
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Unexpected EOF" << dotendl;
    case PrjNull:
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Illegal null character" << dotendl;
    case PrjBadString:
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Unterminated string constant" << dotendl;
    case PrjString:
    case PrjName:
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Unexpected token: " << squote(prj_lex_val) << dotendl;
    case PrjClose:
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Unexpected ')'" << dotendl;
    case PrjOpen:
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Unexpected '('" << dotendl;
    }
    return NoError;
}

PrConstSexpPtrError ProjectDescriptor::read_list_token(PrjTokenType tok, int start_index)
{
    const Sexp *s1, *s2;
    Sexp *s;
    int start_line = prj_lineno;

    switch (tok) {
    case PrjEof: case PrjNull: case PrjBadString:
	return read_parse_bad_token (tok);

    case PrjString:
    case PrjName:
	Return_if_fail(s2 << read_new_estring (tok));

	Return_if_fail(s1 << read_list(start_index));

	s = new Sexp(s2, s1, prj_lineno);

	s->set_start (start_index);
	s->set_end (prj_lex_cur_index);

	return s;

    case PrjClose:
	s = new Sexp (prj_lineno);

	s->set_start (start_index);
	s->set_end (prj_lex_cur_index);

	return s;

    case PrjOpen:
	break;
    }

    Return_if_fail(s1 << read_list(prj_lex_this_index));
    Return_if_fail(s2 << read_list(start_index));

    s = new Sexp(s1, s2, start_line);

    s->set_start (start_index);
    s->set_end (prj_lex_cur_index);

    return s;
}

PrConstSexpPtrError ProjectDescriptor::read_list(int offset)
{
    return read_list_token (prj_lex(), offset);
}

PrVoidError ProjectDescriptor::read_list_checked(size_t offset)
{
    const PrjFields* pf;
    PrjTokenType tok;
    const Sexp* s;
    int h;

    tok = prj_lex();

    if (tok != PrjName)
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Expecting label at head of list" << dotendl;

    pf = prj_lookup_func(prj_lex_val, prj_lex_len);
    h  = prj_lookup_hash(prj_lex_val, prj_lex_len);

    if (!pf)
	pthrow prcserror << "Unexpected label " << squote (prj_lex_val) << dotendl;

    if (_prj_entry_found[h] && h != prj_lookup_hash ("Files", strlen ("Files")))
	pthrow prcserror << fileline(_prj_source_name, prj_lineno)
			<< "Duplicate project file entry" << dotendl;

    _prj_entry_found[h] = true;

    if (pf->parse_func) {
	Return_if_fail (s << read_list_token (tok, offset));

	/* I finally found a use for the ->* operator, fear it. */
	/*Return_if_fail( (this ->* (pf->parse_func)) (s) );*/
	Return_if_fail( (this ->* (pf->parse_func)) (s) );

	delete s;
    } else {
	Return_if_fail( (this ->* (pf->read_func)) (offset) );
    }

    return NoError;
}
