/* Implementation for file attribute munging features.

   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, 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.  */

#include "cvs.h"
#include "getline.h"
#include "fileattr.h"
#include <assert.h>

static void fileattr_read PROTO ((void));
static int writeattr_proc PROTO ((Node *, void *));

/* Where to look for CVSREP_FILEATTR.  */
static char *fileattr_stored_repos;

/* The in-memory attributes.  */
static List *attrlist;
static char *fileattr_default_attrs;
/* We have already tried to read attributes and failed in this directory
   (for example, there is no CVSREP_FILEATTR file).  */
static int attr_read_attempted;

/* Have the in-memory attributes been modified since we read them?  */
static int attrs_modified;

/* More in-memory attributes: linked list of unrecognized
   fileattr lines.  We pass these on unchanged.  */
struct unrecog {
    char *line;
    struct unrecog *next;
};
static struct unrecog *unrecog_head;

/* Note that if noone calls fileattr_get, this is very cheap.  No stat(),
   no open(), no nothing.  */
void
fileattr_startdir (repos)
    char *repos;
{
    assert (fileattr_stored_repos == NULL);
    fileattr_stored_repos = xstrdup (repos);
    assert (attrlist == NULL);
    attr_read_attempted = 0;
    assert (unrecog_head == NULL);
}

static void
fileattr_delproc (node)
    Node *node;
{
    assert (node->data != NULL);
    free (node->data);
    node->data = NULL;
}

/* Read all the attributes for the current directory into memory.  */
static void
fileattr_read ()
{
    char *fname;
    FILE *fp;
    char *line = NULL;
    size_t line_len = 0;

    /* If there are no attributes, don't waste time repeatedly looking
       for the CVSREP_FILEATTR file.  */
    if (attr_read_attempted)
	return;

    /* If NULL was passed to fileattr_startdir, then it isn't kosher to look
       at attributes.  */
    assert (fileattr_stored_repos != NULL);

    fname = xmalloc (strlen (fileattr_stored_repos)
		     + 1
		     + sizeof (CVSREP_FILEATTR)
		     + 1);

    strcpy (fname, fileattr_stored_repos);
    strcat (fname, "/");
    strcat (fname, CVSREP_FILEATTR);

    attr_read_attempted = 1;
    fp = CVS_FOPEN (fname, FOPEN_BINARY_READ);
    if (fp == NULL)
    {
	if (!existence_error (errno))
	    error (0, errno, "cannot read %s", fname);
	free (fname);
	return;
    }
    attrlist = getlist ();
    while (1) {
	int nread;
	nread = getline (&line, &line_len, fp);
	if (nread < 0)
	    break;
	/* Remove trailing newline.  */
	line[nread - 1] = '\0';
	if (line[0] == 'F')
	{
	    char *p;
	    Node *newnode;

	    p = strchr (line, '\t');
	    if (p == NULL)
		error (1, 0,
		       "file attribute database corruption: tab missing in %s",
		       fname);
	    *p++ = '\0';
	    newnode = getnode ();
	    newnode->type = FILEATTR;
	    newnode->delproc = fileattr_delproc;
	    newnode->key = xstrdup (line + 1);
	    newnode->data = xstrdup (p);
	    if (addnode (attrlist, newnode) != 0)
		/* If the same filename appears twice in the file, discard
		   any line other than the first for that filename.  This
		   is the way that CVS has behaved since file attributes
		   were first introduced.  */
		freenode (newnode);
	}
	else if (line[0] == 'D')
	{
	    char *p;
	    /* Currently nothing to skip here, but for future expansion,
	       ignore anything located here.  */
	    p = strchr (line, '\t');
	    if (p == NULL)
		error (1, 0,
		       "file attribute database corruption: tab missing in %s",
		       fname);
	    ++p;
	    fileattr_default_attrs = xstrdup (p);
	}
	else
	{
	    /* Unrecognized type, we want to just preserve the line without
	       changing it, for future expansion.  */
	    struct unrecog *new;

	    new = (struct unrecog *) xmalloc (sizeof (struct unrecog));
	    new->line = xstrdup (line);
	    new->next = unrecog_head;
	    unrecog_head = new;
	}
    }
    if (ferror (fp))
	error (0, errno, "cannot read %s", fname);
    if (line != NULL)
	free (line);
    if (fclose (fp) < 0)
	error (0, errno, "cannot close %s", fname);
    attrs_modified = 0;
    free (fname);
}

char *
fileattr_get (filename, attrname)
    const char *filename;
    const char *attrname;
{
    Node *node;
    size_t attrname_len = strlen (attrname);
    char *p;

    if (attrlist == NULL)
	fileattr_read ();
    if (attrlist == NULL)
	/* Either nothing has any attributes, or fileattr_read already printed
	   an error message.  */
	return NULL;

    if (filename == NULL)
	p = fileattr_default_attrs;
    else
    {
	node = findnode (attrlist, filename);
	if (node == NULL)
	    /* A file not mentioned has no attributes.  */
	    return NULL;
	p = node->data;
    }
    while (p)
    {
	if (strncmp (attrname, p, attrname_len) == 0
	    && p[attrname_len] == '=')
	{
	    /* Found it.  */
	    return p + attrname_len + 1;
	}
	p = strchr (p, ';');
	if (p == NULL)
	    break;
	++p;
    }
    /* The file doesn't have this attribute.  */
    return NULL;
}

char *
fileattr_get0 (filename, attrname)
    const char *filename;
    const char *attrname;
{
    char *cp;
    char *cpend;
    char *retval;

    cp = fileattr_get (filename, attrname);
    if (cp == NULL)
	return NULL;
    cpend = strchr (cp, ';');
    if (cpend == NULL)
	cpend = cp + strlen (cp);
    retval = xmalloc (cpend - cp + 1);
    strncpy (retval, cp, cpend - cp);
    retval[cpend - cp] = '\0';
    return retval;
}

char *
fileattr_modify (list, attrname, attrval, namevalsep, entsep)
    char *list;
    const char *attrname;
    const char *attrval;
    int namevalsep;
    int entsep;
{
    char *retval;
    char *rp;
    size_t attrname_len = strlen (attrname);

    /* Portion of list before the attribute to be replaced.  */
    char *pre;
    char *preend;
    /* Portion of list after the attribute to be replaced.  */
    char *post;

    char *p;
    char *p2;

    p = list;
    pre = list;
    preend = NULL;
    /* post is NULL unless set otherwise.  */
    post = NULL;
    p2 = NULL;
    if (list != NULL)
    {
	while (1) {
	    p2 = strchr (p, entsep);
	    if (p2 == NULL)
	    {
		p2 = p + strlen (p);
		if (preend == NULL)
		    preend = p2;
	    }
	    else
		++p2;
	    if (strncmp (attrname, p, attrname_len) == 0
		&& p[attrname_len] == namevalsep)
	    {
		/* Found it.  */
		preend = p;
		if (preend > list)
		    /* Don't include the preceding entsep.  */
		    --preend;

		post = p2;
	    }
	    if (p2[0] == '\0')
		break;
	    p = p2;
	}
    }
    if (post == NULL)
	post = p2;

    if (preend == pre && attrval == NULL && post == p2)
	return NULL;

    retval = xmalloc ((preend - pre)
		      + 1
		      + (attrval == NULL ? 0 : (attrname_len + 1
						+ strlen (attrval)))
		      + 1
		      + (p2 - post)
		      + 1);
    if (preend != pre)
    {
	strncpy (retval, pre, preend - pre);
	rp = retval + (preend - pre);
	if (attrval != NULL)
	    *rp++ = entsep;
	*rp = '\0';
    }
    else
	retval[0] = '\0';
    if (attrval != NULL)
    {
	strcat (retval, attrname);
	rp = retval + strlen (retval);
	*rp++ = namevalsep;
	strcpy (rp, attrval);
    }
    if (post != p2)
    {
	rp = retval + strlen (retval);
	if (preend != pre || attrval != NULL)
	    *rp++ = entsep;
	strncpy (rp, post, p2 - post);
	rp += p2 - post;
	*rp = '\0';
    }
    return retval;
}

void
fileattr_set (filename, attrname, attrval)
    const char *filename;
    const char *attrname;
    const char *attrval;
{
    Node *node;
    char *p;

    if (filename == NULL)
    {
	p = fileattr_modify (fileattr_default_attrs, attrname, attrval,
			     '=', ';');
	if (fileattr_default_attrs != NULL)
	    free (fileattr_default_attrs);
	fileattr_default_attrs = p;
	attrs_modified = 1;
	return;
    }
    if (attrlist == NULL)
	fileattr_read ();
    if (attrlist == NULL)
    {
	/* Not sure this is a graceful way to handle things
	   in the case where fileattr_read was unable to read the file.  */
        /* No attributes existed previously.  */
	attrlist = getlist ();
    }

    node = findnode (attrlist, filename);
    if (node == NULL)
    {
	if (attrval == NULL)
	    /* Attempt to remove an attribute which wasn't there.  */
	    return;

	/* First attribute for this file.  */
	node = getnode ();
	node->type = FILEATTR;
	node->delproc = fileattr_delproc;
	node->key = xstrdup (filename);
	node->data = xmalloc (strlen (attrname) + 1 + strlen (attrval) + 1);
	strcpy (node->data, attrname);
	strcat (node->data, "=");
	strcat (node->data, attrval);
	addnode (attrlist, node);
    }

    p = fileattr_modify (node->data, attrname, attrval, '=', ';');
    if (p == NULL)
	delnode (node);
    else
    {
	free (node->data);
	node->data = p;
    }

    attrs_modified = 1;
}

char *
fileattr_getall (filename)
    const char *filename;
{
    Node *node;
    char *p;

    if (attrlist == NULL)
	fileattr_read ();
    if (attrlist == NULL)
	/* Either nothing has any attributes, or fileattr_read already printed
	   an error message.  */
	return NULL;

    if (filename == NULL)
	p = fileattr_default_attrs;
    else
    {
	node = findnode (attrlist, filename);
	if (node == NULL)
	    /* A file not mentioned has no attributes.  */
	    return NULL;
	p = node->data;
    }
    return xstrdup (p);
}

void
fileattr_setall (filename, attrs)
    const char *filename;
    const char *attrs;
{
    Node *node;

    if (filename == NULL)
    {
	if (fileattr_default_attrs != NULL)
	    free (fileattr_default_attrs);
	fileattr_default_attrs = xstrdup (attrs);
	attrs_modified = 1;
	return;
    }
    if (attrlist == NULL)
	fileattr_read ();
    if (attrlist == NULL)
    {
	/* Not sure this is a graceful way to handle things
	   in the case where fileattr_read was unable to read the file.  */
        /* No attributes existed previously.  */
	attrlist = getlist ();
    }

    node = findnode (attrlist, filename);
    if (node == NULL)
    {
	/* The file had no attributes.  Add them if we have any to add.  */
	if (attrs != NULL)
	{
	    node = getnode ();
	    node->type = FILEATTR;
	    node->delproc = fileattr_delproc;
	    node->key = xstrdup (filename);
	    node->data = xstrdup (attrs);
	    addnode (attrlist, node);
	}
    }
    else
    {
	if (attrs == NULL)
	    delnode (node);
	else
	{
	    free (node->data);
	    node->data = xstrdup (attrs);
	}
    }

    attrs_modified = 1;
}

void
fileattr_newfile (filename)
    const char *filename;
{
    Node *node;

    if (attrlist == NULL)
	fileattr_read ();

    if (fileattr_default_attrs == NULL)
	return;

    if (attrlist == NULL)
    {
	/* Not sure this is a graceful way to handle things
	   in the case where fileattr_read was unable to read the file.  */
        /* No attributes existed previously.  */
	attrlist = getlist ();
    }

    node = getnode ();
    node->type = FILEATTR;
    node->delproc = fileattr_delproc;
    node->key = xstrdup (filename);
    node->data = xstrdup (fileattr_default_attrs);
    addnode (attrlist, node);
    attrs_modified = 1;
}

static int
writeattr_proc (node, data)
    Node *node;
    void *data;
{
    FILE *fp = (FILE *)data;
    fputs ("F", fp);
    fputs (node->key, fp);
    fputs ("\t", fp);
    fputs (node->data, fp);
    fputs ("\012", fp);
    return 0;
}

void
fileattr_write ()
{
    FILE *fp;
    char *fname;
    mode_t omask;
    struct unrecog *p;

    if (!attrs_modified)
	return;

    if (noexec)
	return;

    /* If NULL was passed to fileattr_startdir, then it isn't kosher to set
       attributes.  */
    assert (fileattr_stored_repos != NULL);

    fname = xmalloc (strlen (fileattr_stored_repos)
		     + 1
		     + sizeof (CVSREP_FILEATTR)
		     + 1);

    strcpy (fname, fileattr_stored_repos);
    strcat (fname, "/");
    strcat (fname, CVSREP_FILEATTR);

    if (list_isempty (attrlist)
	&& fileattr_default_attrs == NULL
	&& unrecog_head == NULL)
    {
	/* There are no attributes.  */
	if (unlink_file (fname) < 0)
	{
	    if (!existence_error (errno))
	    {
		error (0, errno, "cannot remove %s", fname);
	    }
	}

	/* Now remove CVSREP directory, if empty.  The main reason we bother
	   is that CVS 1.6 and earlier will choke if a CVSREP directory
	   exists, so provide the user a graceful way to remove it.  */
	strcpy (fname, fileattr_stored_repos);
	strcat (fname, "/");
	strcat (fname, CVSREP);
	if (CVS_RMDIR (fname) < 0)
	{
	    if (errno != ENOTEMPTY

		/* Don't know why we would be here if there is no CVSREP
		   directory, but it seemed to be happening anyway, so
		   check for it.  */
		&& !existence_error (errno))
		error (0, errno, "cannot remove %s", fname);
	}

	free (fname);
	return;
    }

    omask = umask (cvsumask);
    fp = CVS_FOPEN (fname, FOPEN_BINARY_WRITE);
    if (fp == NULL)
    {
	if (existence_error (errno))
	{
	    /* Maybe the CVSREP directory doesn't exist.  Try creating it.  */
	    char *repname;

	    repname = xmalloc (strlen (fileattr_stored_repos)
			       + 1
			       + sizeof (CVSREP)
			       + 1);
	    strcpy (repname, fileattr_stored_repos);
	    strcat (repname, "/");
	    strcat (repname, CVSREP);

	    if (CVS_MKDIR (repname, 0777) < 0 && errno != EEXIST)
	    {
		error (0, errno, "cannot make directory %s", repname);
		(void) umask (omask);
		free (repname);
		return;
	    }
	    free (repname);

	    fp = CVS_FOPEN (fname, FOPEN_BINARY_WRITE);
	}
	if (fp == NULL)
	{
	    error (0, errno, "cannot write %s", fname);
	    (void) umask (omask);
	    return;
	}
    }
    (void) umask (omask);

    /* First write the "F" attributes.  */
    walklist (attrlist, writeattr_proc, fp);

    /* Then the "D" attribute.  */
    if (fileattr_default_attrs != NULL)
    {
	fputs ("D\t", fp);
	fputs (fileattr_default_attrs, fp);
	fputs ("\012", fp);
    }

    /* Then any other attributes.  */
    for (p = unrecog_head; p != NULL; p = p->next)
    {
	fputs (p->line, fp);
	fputs ("\012", fp);
    }

    if (fclose (fp) < 0)
	error (0, errno, "cannot close %s", fname);
    attrs_modified = 0;
    free (fname);
}

void
fileattr_free ()
{
    /* Note that attrs_modified will ordinarily be zero, but there are
       a few cases in which fileattr_write will fail to zero it (if
       noexec is set, or error conditions).  This probably is the way
       it should be.  */
    dellist (&attrlist);
    if (fileattr_stored_repos != NULL)
	free (fileattr_stored_repos);
    fileattr_stored_repos = NULL;
    if (fileattr_default_attrs != NULL)
	free (fileattr_default_attrs);
    fileattr_default_attrs = NULL;
    while (unrecog_head)
    {
	struct unrecog *p = unrecog_head;
	unrecog_head = p->next;
	free (p->line);
	free (p);
    }
}
