1/* Implementation for file attribute munging features.
2
3   This program is free software; you can redistribute it and/or modify
4   it under the terms of the GNU General Public License as published by
5   the Free Software Foundation; either version 2, or (at your option)
6   any later version.
7
8   This program is distributed in the hope that it will be useful,
9   but WITHOUT ANY WARRANTY; without even the implied warranty of
10   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11   GNU General Public License for more details.  */
12
13#include "cvs.h"
14#include "getline.h"
15#include "fileattr.h"
16#include <assert.h>
17
18static void fileattr_read PROTO ((void));
19static int writeattr_proc PROTO ((Node *, void *));
20
21/* Where to look for CVSREP_FILEATTR.  */
22static char *fileattr_stored_repos;
23
24/* The in-memory attributes.  */
25static List *attrlist;
26static char *fileattr_default_attrs;
27/* We have already tried to read attributes and failed in this directory
28   (for example, there is no CVSREP_FILEATTR file).  */
29static int attr_read_attempted;
30
31/* Have the in-memory attributes been modified since we read them?  */
32static int attrs_modified;
33
34/* More in-memory attributes: linked list of unrecognized
35   fileattr lines.  We pass these on unchanged.  */
36struct unrecog {
37    char *line;
38    struct unrecog *next;
39};
40static struct unrecog *unrecog_head;
41
42/* Note that if noone calls fileattr_get, this is very cheap.  No stat(),
43   no open(), no nothing.  */
44void
45fileattr_startdir (repos)
46    const char *repos;
47{
48    assert (fileattr_stored_repos == NULL);
49    fileattr_stored_repos = xstrdup (repos);
50    assert (attrlist == NULL);
51    attr_read_attempted = 0;
52    assert (unrecog_head == NULL);
53}
54
55static void
56fileattr_delproc (node)
57    Node *node;
58{
59    assert (node->data != NULL);
60    free (node->data);
61    node->data = NULL;
62}
63
64/* Read all the attributes for the current directory into memory.  */
65static void
66fileattr_read ()
67{
68    char *fname;
69    FILE *fp;
70    char *line = NULL;
71    size_t line_len = 0;
72
73    /* If there are no attributes, don't waste time repeatedly looking
74       for the CVSREP_FILEATTR file.  */
75    if (attr_read_attempted)
76	return;
77
78    /* If NULL was passed to fileattr_startdir, then it isn't kosher to look
79       at attributes.  */
80    assert (fileattr_stored_repos != NULL);
81
82    fname = xmalloc (strlen (fileattr_stored_repos)
83		     + 1
84		     + sizeof (CVSREP_FILEATTR)
85		     + 1);
86
87    strcpy (fname, fileattr_stored_repos);
88    strcat (fname, "/");
89    strcat (fname, CVSREP_FILEATTR);
90
91    attr_read_attempted = 1;
92    fp = CVS_FOPEN (fname, FOPEN_BINARY_READ);
93    if (fp == NULL)
94    {
95	if (!existence_error (errno))
96	    error (0, errno, "cannot read %s", fname);
97	free (fname);
98	return;
99    }
100    attrlist = getlist ();
101    while (1) {
102	int nread;
103	nread = getline (&line, &line_len, fp);
104	if (nread < 0)
105	    break;
106	/* Remove trailing newline.  */
107	line[nread - 1] = '\0';
108	if (line[0] == 'F')
109	{
110	    char *p;
111	    Node *newnode;
112
113	    p = strchr (line, '\t');
114	    if (p == NULL)
115		error (1, 0,
116		       "file attribute database corruption: tab missing in %s",
117		       fname);
118	    *p++ = '\0';
119	    newnode = getnode ();
120	    newnode->type = FILEATTR;
121	    newnode->delproc = fileattr_delproc;
122	    newnode->key = xstrdup (line + 1);
123	    newnode->data = xstrdup (p);
124	    if (addnode (attrlist, newnode) != 0)
125		/* If the same filename appears twice in the file, discard
126		   any line other than the first for that filename.  This
127		   is the way that CVS has behaved since file attributes
128		   were first introduced.  */
129		freenode (newnode);
130	}
131	else if (line[0] == 'D')
132	{
133	    char *p;
134	    /* Currently nothing to skip here, but for future expansion,
135	       ignore anything located here.  */
136	    p = strchr (line, '\t');
137	    if (p == NULL)
138		error (1, 0,
139		       "file attribute database corruption: tab missing in %s",
140		       fname);
141	    ++p;
142	    if (fileattr_default_attrs) free (fileattr_default_attrs);
143	    fileattr_default_attrs = xstrdup (p);
144	}
145	else
146	{
147	    /* Unrecognized type, we want to just preserve the line without
148	       changing it, for future expansion.  */
149	    struct unrecog *new;
150
151	    new = (struct unrecog *) xmalloc (sizeof (struct unrecog));
152	    new->line = xstrdup (line);
153	    new->next = unrecog_head;
154	    unrecog_head = new;
155	}
156    }
157    if (ferror (fp))
158	error (0, errno, "cannot read %s", fname);
159    if (line != NULL)
160	free (line);
161    if (fclose (fp) < 0)
162	error (0, errno, "cannot close %s", fname);
163    attrs_modified = 0;
164    free (fname);
165}
166
167char *
168fileattr_get (filename, attrname)
169    const char *filename;
170    const char *attrname;
171{
172    Node *node;
173    size_t attrname_len = strlen (attrname);
174    char *p;
175
176    if (attrlist == NULL)
177	fileattr_read ();
178    if (attrlist == NULL)
179	/* Either nothing has any attributes, or fileattr_read already printed
180	   an error message.  */
181	return NULL;
182
183    if (filename == NULL)
184	p = fileattr_default_attrs;
185    else
186    {
187	node = findnode (attrlist, filename);
188	if (node == NULL)
189	    /* A file not mentioned has no attributes.  */
190	    return NULL;
191	p = node->data;
192    }
193    while (p)
194    {
195	if (strncmp (attrname, p, attrname_len) == 0
196	    && p[attrname_len] == '=')
197	{
198	    /* Found it.  */
199	    return p + attrname_len + 1;
200	}
201	p = strchr (p, ';');
202	if (p == NULL)
203	    break;
204	++p;
205    }
206    /* The file doesn't have this attribute.  */
207    return NULL;
208}
209
210char *
211fileattr_get0 (filename, attrname)
212    const char *filename;
213    const char *attrname;
214{
215    char *cp;
216    char *cpend;
217    char *retval;
218
219    cp = fileattr_get (filename, attrname);
220    if (cp == NULL)
221	return NULL;
222    cpend = strchr (cp, ';');
223    if (cpend == NULL)
224	cpend = cp + strlen (cp);
225    retval = xmalloc (cpend - cp + 1);
226    strncpy (retval, cp, cpend - cp);
227    retval[cpend - cp] = '\0';
228    return retval;
229}
230
231char *
232fileattr_modify (list, attrname, attrval, namevalsep, entsep)
233    char *list;
234    const char *attrname;
235    const char *attrval;
236    int namevalsep;
237    int entsep;
238{
239    char *retval;
240    char *rp;
241    size_t attrname_len = strlen (attrname);
242
243    /* Portion of list before the attribute to be replaced.  */
244    char *pre;
245    char *preend;
246    /* Portion of list after the attribute to be replaced.  */
247    char *post;
248
249    char *p;
250    char *p2;
251
252    p = list;
253    pre = list;
254    preend = NULL;
255    /* post is NULL unless set otherwise.  */
256    post = NULL;
257    p2 = NULL;
258    if (list != NULL)
259    {
260	while (1) {
261	    p2 = strchr (p, entsep);
262	    if (p2 == NULL)
263	    {
264		p2 = p + strlen (p);
265		if (preend == NULL)
266		    preend = p2;
267	    }
268	    else
269		++p2;
270	    if (strncmp (attrname, p, attrname_len) == 0
271		&& p[attrname_len] == namevalsep)
272	    {
273		/* Found it.  */
274		preend = p;
275		if (preend > list)
276		    /* Don't include the preceding entsep.  */
277		    --preend;
278
279		post = p2;
280	    }
281	    if (p2[0] == '\0')
282		break;
283	    p = p2;
284	}
285    }
286    if (post == NULL)
287	post = p2;
288
289    if (preend == pre && attrval == NULL && post == p2)
290	return NULL;
291
292    retval = xmalloc ((preend - pre)
293		      + 1
294		      + (attrval == NULL ? 0 : (attrname_len + 1
295						+ strlen (attrval)))
296		      + 1
297		      + (p2 - post)
298		      + 1);
299    if (preend != pre)
300    {
301	strncpy (retval, pre, preend - pre);
302	rp = retval + (preend - pre);
303	if (attrval != NULL)
304	    *rp++ = entsep;
305	*rp = '\0';
306    }
307    else
308	retval[0] = '\0';
309    if (attrval != NULL)
310    {
311	strcat (retval, attrname);
312	rp = retval + strlen (retval);
313	*rp++ = namevalsep;
314	strcpy (rp, attrval);
315    }
316    if (post != p2)
317    {
318	rp = retval + strlen (retval);
319	if (preend != pre || attrval != NULL)
320	    *rp++ = entsep;
321	strncpy (rp, post, p2 - post);
322	rp += p2 - post;
323	*rp = '\0';
324    }
325    return retval;
326}
327
328void
329fileattr_set (filename, attrname, attrval)
330    const char *filename;
331    const char *attrname;
332    const char *attrval;
333{
334    Node *node;
335    char *p;
336
337    if (filename == NULL)
338    {
339	p = fileattr_modify (fileattr_default_attrs, attrname, attrval,
340			     '=', ';');
341	if (fileattr_default_attrs != NULL)
342	    free (fileattr_default_attrs);
343	fileattr_default_attrs = p;
344	attrs_modified = 1;
345	return;
346    }
347    if (attrlist == NULL)
348	fileattr_read ();
349    if (attrlist == NULL)
350    {
351	/* Not sure this is a graceful way to handle things
352	   in the case where fileattr_read was unable to read the file.  */
353        /* No attributes existed previously.  */
354	attrlist = getlist ();
355    }
356
357    node = findnode (attrlist, filename);
358    if (node == NULL)
359    {
360	if (attrval == NULL)
361	    /* Attempt to remove an attribute which wasn't there.  */
362	    return;
363
364	/* First attribute for this file.  */
365	node = getnode ();
366	node->type = FILEATTR;
367	node->delproc = fileattr_delproc;
368	node->key = xstrdup (filename);
369	node->data = xmalloc (strlen (attrname) + 1 + strlen (attrval) + 1);
370	strcpy (node->data, attrname);
371	strcat (node->data, "=");
372	strcat (node->data, attrval);
373	addnode (attrlist, node);
374    }
375
376    p = fileattr_modify (node->data, attrname, attrval, '=', ';');
377    if (p == NULL)
378	delnode (node);
379    else
380    {
381	free (node->data);
382	node->data = p;
383    }
384
385    attrs_modified = 1;
386}
387
388char *
389fileattr_getall (filename)
390    const char *filename;
391{
392    Node *node;
393    char *p;
394
395    if (attrlist == NULL)
396	fileattr_read ();
397    if (attrlist == NULL)
398	/* Either nothing has any attributes, or fileattr_read already printed
399	   an error message.  */
400	return NULL;
401
402    if (filename == NULL)
403	p = fileattr_default_attrs;
404    else
405    {
406	node = findnode (attrlist, filename);
407	if (node == NULL)
408	    /* A file not mentioned has no attributes.  */
409	    return NULL;
410	p = node->data;
411    }
412    return xstrdup (p);
413}
414
415void
416fileattr_setall (filename, attrs)
417    const char *filename;
418    const char *attrs;
419{
420    Node *node;
421
422    if (filename == NULL)
423    {
424	if (fileattr_default_attrs != NULL)
425	    free (fileattr_default_attrs);
426	fileattr_default_attrs = xstrdup (attrs);
427	attrs_modified = 1;
428	return;
429    }
430    if (attrlist == NULL)
431	fileattr_read ();
432    if (attrlist == NULL)
433    {
434	/* Not sure this is a graceful way to handle things
435	   in the case where fileattr_read was unable to read the file.  */
436        /* No attributes existed previously.  */
437	attrlist = getlist ();
438    }
439
440    node = findnode (attrlist, filename);
441    if (node == NULL)
442    {
443	/* The file had no attributes.  Add them if we have any to add.  */
444	if (attrs != NULL)
445	{
446	    node = getnode ();
447	    node->type = FILEATTR;
448	    node->delproc = fileattr_delproc;
449	    node->key = xstrdup (filename);
450	    node->data = xstrdup (attrs);
451	    addnode (attrlist, node);
452	}
453    }
454    else
455    {
456	if (attrs == NULL)
457	    delnode (node);
458	else
459	{
460	    free (node->data);
461	    node->data = xstrdup (attrs);
462	}
463    }
464
465    attrs_modified = 1;
466}
467
468void
469fileattr_newfile (filename)
470    const char *filename;
471{
472    Node *node;
473
474    if (attrlist == NULL)
475	fileattr_read ();
476
477    if (fileattr_default_attrs == NULL)
478	return;
479
480    if (attrlist == NULL)
481    {
482	/* Not sure this is a graceful way to handle things
483	   in the case where fileattr_read was unable to read the file.  */
484        /* No attributes existed previously.  */
485	attrlist = getlist ();
486    }
487
488    node = getnode ();
489    node->type = FILEATTR;
490    node->delproc = fileattr_delproc;
491    node->key = xstrdup (filename);
492    node->data = xstrdup (fileattr_default_attrs);
493    addnode (attrlist, node);
494    attrs_modified = 1;
495}
496
497static int
498writeattr_proc (node, data)
499    Node *node;
500    void *data;
501{
502    FILE *fp = (FILE *)data;
503    fputs ("F", fp);
504    fputs (node->key, fp);
505    fputs ("\t", fp);
506    fputs (node->data, fp);
507    fputs ("\012", fp);
508    return 0;
509}
510
511void
512fileattr_write ()
513{
514    FILE *fp;
515    char *fname;
516    mode_t omask;
517    struct unrecog *p;
518
519    if (!attrs_modified)
520	return;
521
522    if (noexec)
523	return;
524
525    /* If NULL was passed to fileattr_startdir, then it isn't kosher to set
526       attributes.  */
527    assert (fileattr_stored_repos != NULL);
528
529    fname = xmalloc (strlen (fileattr_stored_repos)
530		     + 1
531		     + sizeof (CVSREP_FILEATTR)
532		     + 1);
533
534    strcpy (fname, fileattr_stored_repos);
535    strcat (fname, "/");
536    strcat (fname, CVSREP_FILEATTR);
537
538    if (list_isempty (attrlist)
539	&& fileattr_default_attrs == NULL
540	&& unrecog_head == NULL)
541    {
542	/* There are no attributes.  */
543	if (unlink_file (fname) < 0)
544	{
545	    if (!existence_error (errno))
546	    {
547		error (0, errno, "cannot remove %s", fname);
548	    }
549	}
550
551	/* Now remove CVSREP directory, if empty.  The main reason we bother
552	   is that CVS 1.6 and earlier will choke if a CVSREP directory
553	   exists, so provide the user a graceful way to remove it.  */
554	strcpy (fname, fileattr_stored_repos);
555	strcat (fname, "/");
556	strcat (fname, CVSREP);
557	if (CVS_RMDIR (fname) < 0)
558	{
559	    if (errno != ENOTEMPTY
560
561		/* Don't know why we would be here if there is no CVSREP
562		   directory, but it seemed to be happening anyway, so
563		   check for it.  */
564		&& !existence_error (errno))
565		error (0, errno, "cannot remove %s", fname);
566	}
567
568	free (fname);
569	return;
570    }
571
572    omask = umask (cvsumask);
573    fp = CVS_FOPEN (fname, FOPEN_BINARY_WRITE);
574    if (fp == NULL)
575    {
576	if (existence_error (errno))
577	{
578	    /* Maybe the CVSREP directory doesn't exist.  Try creating it.  */
579	    char *repname;
580
581	    repname = xmalloc (strlen (fileattr_stored_repos)
582			       + 1
583			       + sizeof (CVSREP)
584			       + 1);
585	    strcpy (repname, fileattr_stored_repos);
586	    strcat (repname, "/");
587	    strcat (repname, CVSREP);
588
589	    if (CVS_MKDIR (repname, 0777) < 0 && errno != EEXIST)
590	    {
591		error (0, errno, "cannot make directory %s", repname);
592		(void) umask (omask);
593		free (fname);
594		free (repname);
595		return;
596	    }
597	    free (repname);
598
599	    fp = CVS_FOPEN (fname, FOPEN_BINARY_WRITE);
600	}
601	if (fp == NULL)
602	{
603	    error (0, errno, "cannot write %s", fname);
604	    (void) umask (omask);
605	    free (fname);
606	    return;
607	}
608    }
609    (void) umask (omask);
610
611    /* First write the "F" attributes.  */
612    walklist (attrlist, writeattr_proc, fp);
613
614    /* Then the "D" attribute.  */
615    if (fileattr_default_attrs != NULL)
616    {
617	fputs ("D\t", fp);
618	fputs (fileattr_default_attrs, fp);
619	fputs ("\012", fp);
620    }
621
622    /* Then any other attributes.  */
623    for (p = unrecog_head; p != NULL; p = p->next)
624    {
625	fputs (p->line, fp);
626	fputs ("\012", fp);
627    }
628
629    if (fclose (fp) < 0)
630	error (0, errno, "cannot close %s", fname);
631    attrs_modified = 0;
632    free (fname);
633}
634
635void
636fileattr_free ()
637{
638    /* Note that attrs_modified will ordinarily be zero, but there are
639       a few cases in which fileattr_write will fail to zero it (if
640       noexec is set, or error conditions).  This probably is the way
641       it should be.  */
642    dellist (&attrlist);
643    if (fileattr_stored_repos != NULL)
644	free (fileattr_stored_repos);
645    fileattr_stored_repos = NULL;
646    if (fileattr_default_attrs != NULL)
647	free (fileattr_default_attrs);
648    fileattr_default_attrs = NULL;
649    while (unrecog_head)
650    {
651	struct unrecog *p = unrecog_head;
652	unrecog_head = p->next;
653	free (p->line);
654	free (p);
655    }
656}
657