// rclean Copyright (c) 2002 Lapo Luchini <lapo@lapo.it>
// $Header: /usr/local/cvsroot/rclean/rclean.c,v 1.14 2002/12/22 17:12:30 lapo Exp $

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
// * Neither the name of Lapo Luchini nor the names of his contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

/*
** $Log: rclean.c,v $
** Revision 1.14  2002/12/22 17:12:30  lapo
** - FreeBSD needs sys/types.h defined before regexp.h, very strange
**
** Revision 1.13  2002/12/22 17:00:25  lapo
** - Removes rclean.h, it was too small to have sense
** - Changed indentation to be more 'standard', in a K&R way
** - Reversed include order, as suggested by Bruce Eckel's books
** - Defined a couple of string to avoid redundancy in the code
** - Generalized the first (and only) key name exception
** - Corrected the bug that did halt the program when a unknown exception key name was found
** - Added command line option to print exceptions list
** - Added line numbers in parse errors of the conf files
**
** Revision 1.12    2002/07/07 17:18:51    lapo
** - Added comments
** - Order of 'exception' options is now preserved
** - Default files are now printed in help
** - Changed help so that '-a' no more occupies more than 80 columns
**
** Revision 1.11    2002/04/29 14:16:14    lapo
** - Added '-a' option to print all lines present in rc.conf, even if with default value
** - Added '-w' option to write to file the output
** - Fixed tab creation for comments
**
** Revision 1.10    2002/04/29 10:54:50    lapo
** - Fixed bug in options code (long lines got no comment)
** - Added generic support for element "exceptions"
** - Added '-h' command line option for 'help'
**
** Revision 1.9    2002/04/28 23:40:18    lapo
** - license relaxed (now it's BSD-style)
** - getopt instead of popt (smaller executable and no required packages)
**
** Revision 1.8    2002/04/28 12:42:14    lapo
** - Added javadoc-style comments
** - Removed extra backslashes
**
** Revision 1.7    2002/04/28 10:58:03    lapo
** - Added header to generated output
** - Removed reference to REG_OKAY (was not present in FreeBSD's regex.h)
**
** Revision 1.6    2002/04/27 23:27:00    lapo
** - Added verbosity options
** - Changed default file values to fit FreeBSD
**
** Revision 1.5    2002/04/27 22:47:04    lapo
** - Added command line parsing using the popt library
** - Added GPL notes in source
**
** Revision 1.4    2002/04/27 21:13:10    lapo
** - Added generic support for keys not present in defaults file
**
** Revision 1.3    2002/04/27 21:02:53    lapo
** - Simplified using regex.h
** - Corrected error in memory allocation (strlen+1 instead of strlen)
**
** Revision 1.2    2002/04/25 07:37-07:48    lapo
** - speedup in final difference printing
**
** Revision 1.1    2002/04/25 00:40-03:26    lapo
** - hardcoded filenames
** - no options
** - strange problems with xntpd_enable and inetd_enable
** - sometimes names get followed by a nonprintable char (|>)
*/

/*** includes ****************************************************************/

#include <sys/types.h>
#include <regex.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>

/*** structures **************************************************************/

/**
 * Structure for an element of rc.conf (a line)
 */
struct element {
  char * key;                   // key
  char * value_def;             // default value
  char * value;                 // value
  struct element * next;        // next key
};

/**
 * Structure for an exception element
 * (and element that is valid but it's not defined in default rc.conf)
 */
struct exception {
  char * match;                 // regular expression to match the key
  char * previous;              // key of the element after which must be put
  regex_t compiled;             // regular expression to match the key
};

/*** settings ****************************************************************/

// copyright text
#define COPYRIGHT "rclean Copyright (c) 2002 Lapo Luchini <lapo@lapo.it>"
// string used as default value when not defined in default file
#define NOT_DEFINED "<not defined>"

// first match is 'key', second match is 'value'
#define RE_DEFAULT "^([a-z0-9_]+)=\"([^\"]*)\""
#define RE_CURRENT "^([a-z0-9_]+)=\"([^\"]*)\""
// max line length in rc.conf files
#define MAX_LINE 256
// debug level: 0 to 4
//     0: no debug
//     1: main phases of the program
//     2: sub-phases
//     3: find results
//     4: parsing infos
#define DEBUG 0

struct exception exceptions[] = {
// match for exception key                       put after this key
    {"^ifconfig_[a-z]+[0-9]+(_alias[0-9]+)?$",   "ifconfig_lo0"}
};
#define NUM_EXCEPTIONS (sizeof(exceptions) / sizeof(exceptions[0]))

/*** program *****************************************************************/

#if DEBUG > 0
    #define DEBUG_POINT_s(v, a) if(DEBUG >= v) fprintf(stderr, "[debug] [%s]\n", a);
    #define DEBUG_POINT_ss(v, a, b) if(DEBUG >= v) fprintf(stderr, "[debug] [%s] [%s]\n", a, b);
    #define DEBUG_POINT_sss(v, a, b, c) if(DEBUG >= v) fprintf(stderr, "[debug] [%s] [%s] [%s]\n", a, b, c);
    #define DEBUG_POINT_ssss(v, a, b, c, d) if(DEBUG >= v) fprintf(stderr, "[debug] [%s] [%s] [%s] [%s]\n", a, b, c, d);
#else
    #define DEBUG_POINT_s(v, a) ;
    #define DEBUG_POINT_ss(v, a, b) ;
    #define DEBUG_POINT_sss(v, a, b, c) ;
    #define DEBUG_POINT_ssss(v, a, b, c, d) ;
#endif

struct element * head, * tail;

/**
 * malloc with error management
 */
void * malloc_chk(size_t size) {
    void * u = malloc(size);
    if(u == NULL) {
	fprintf(stderr, "Cannot allocate memory [%d bytes].\n", size);
	exit(2);
    }
    return u;
}

/**
 * Creates a new element, given the content
 * @param key key of the element (it is copied)
 * @param value value of the element (it is copied and used both for value and value_def)
 * @return the new element, dynamically allocated
 */
struct element * element_new(const char * key, const char * value) {
    struct element * u = malloc_chk(sizeof(struct element));
    DEBUG_POINT_sss(1, "element_new", key, value);
    u->key = strcpy(malloc_chk(sizeof(char) * (strlen(key) + 1)), key);
    u->value = strcpy(malloc_chk(sizeof(char) * (strlen(value) + 1)), value);
    u->value_def = u->value;
    u->next = 0;
    return u;
}

/**
 * Creates a new element and add it to the list
 * @param key key of the element (it is copied)
 * @param value value of the element (it is copied and used both for 'value' and 'value_def' fields)
 * @return the new element (that is also the tail of the list)
 */
struct element * element_add(const char * key, const char * value) {
    DEBUG_POINT_sss(2, "element_add", key, value);
    if (!head) {
	head = element_new(key, value);
	tail = head;
    } else {
	tail->next = element_new(key, value);
	tail = tail->next;
    }
    return tail;
}

/**
 * Search the list for an element, given the key
 * @param key key of the element
 * @return the first element with such a key, NULL if not found
 */
struct element * element_find(const char * key) {
    struct element * el = head;
    DEBUG_POINT_ss(2, "element_find", key);
    while((el) && (strcmp(el->key, key) != 0))
	el = el->next;
    if(!el) {
	DEBUG_POINT_ssss(3, "element_find NOT FOUND", el->key, el->value, el->value_def);
	return NULL;
    }
    DEBUG_POINT_ssss(3, "element_find FOUND", el->key, el->value, el->value_def);
    return el;
}

/**
 * Search the list for an element and adds a new element after it
 * appends the new element if previous_key is not found
 * @param key key of the element
 * @param value value of the element (it is copied and used only for 'value' field)
 * @return the new element
 */
struct element * element_insert(const char * previous_key, const char * new_key) {
    struct element * el = element_find(previous_key), * new_el;
    DEBUG_POINT_sss(1, "element_insert", previous_key, new_key);
    if(el) {
	DEBUG_POINT_ss(2, "element_insert INSERTED after", el->key);
	new_el = element_new(new_key, NOT_DEFINED);
	new_el->next = el->next;
	el->next = new_el;
    } else {
	DEBUG_POINT_ss(2, "element_insert APPEND after", tail->key);
	new_el = element_add(new_key, NOT_DEFINED);
    }
    return new_el;
}

/**
 * Search the list for an element and changes it's value
 * appends a new element if key is not found
 * @param key key of the element
 * @param value value of the element (it is copied and used only for 'value' field)
 * @return the modified element
 */
struct element * element_change(const char * key, const char * value) {
    struct element * el = element_find(key);
    if(!el) { // if element doesn't exist let's check for a known exception
	int i, err, found;
	found = 0;
	for(i = 0; (i < NUM_EXCEPTIONS) && !found; i++)
	    if((err = regexec(&exceptions[i].compiled, key, 0, NULL, 0)) == 0 /*REG_OKAY*/) {
		el = element_insert(exceptions[i].previous, key);
		exceptions[i].previous = strcpy(malloc_chk(sizeof(char) * (strlen(key) + 1)), key);
		found = 1;
	    } else if(err != REG_NOMATCH) {
		int err_dim = regerror(err, &exceptions[i].compiled, NULL, 0);
		char * err_text = malloc_chk(sizeof(char) * err_dim);
		regerror(err, &exceptions[i].compiled, err_text, err_dim);
		fprintf(stderr, "Cannot execute regular expression [%s] on [%s]: %s.\n", exceptions[i].match, key, err_text);
		exit(4);
	    }
	if(!found)
	    el = element_add(key, NOT_DEFINED);
    }
    DEBUG_POINT_sss(1, "element_change", key, value);
    DEBUG_POINT_ssss(2, "element_change PRE", el->key, el->value, el->value_def);
    if (el->value != el->value_def)
	free(el->value);
    el->value = strcpy(malloc_chk(sizeof(char) * (strlen(value) + 1)), value);
    DEBUG_POINT_ssss(2, "element_change POST", el->key, el->value, el->value_def);
    return el;
}

/**
 * Compiles a regular expression with error management
 * @param expression the regular expression to be compiled
 * @param compiled the struct to fill with compiled expression
 */
void re_compile(regex_t * compiled, const char * expression, int options) {
    int err;
    if(err = regcomp(compiled, expression, options)) {
	int err_dim = regerror(err, compiled, NULL, 0);
	char * err_text = malloc_chk(sizeof(char) * err_dim);
	regerror(err, compiled, err_text, err_dim);
	fprintf(stderr, "Cannot compile regular expression [%s]: %s.\n", expression, err_text);
	exit(5);
    }
}

/**
 * Opens a text file for reading, with error management
 * @param filename filename of the file to be opened
 * @param mode file mode (see "man 3 fopen")
 * @return the opened file
 */
FILE * file_open(const char * filename, const char * mode) {
    FILE * u;
    if ((u = fopen(filename, mode)) == NULL) {
	fprintf(stderr, "Cannot open default file [%s].\n", filename);
	exit(6);
    }
    return u;
}

/**
 * Parse a line
 * @param re regular expression used to parse the line
 * @param line line to be parsed (it modified)
 * @param funct function to be called with the two parsed parameters (as key and value, respectively)
 */
void line_parse(regex_t * re, char * line, struct element * (* funct)(const char *, const char *), unsigned int linenum) {
    int err, len;
    regmatch_t match[3];
    if(!line)
	return;
    DEBUG_POINT_ss(4, "parsing line", line)
    if((err = regexec(re, line, 3, match, 0)) != 0 /*REG_OKAY*/) {
	if(err == REG_NOMATCH)
	    return;
	else {
	    int err_dim = regerror(err, re, NULL, 0);
	    char * err_text = malloc_chk(sizeof(char) * err_dim);
	    regerror(err, re, err_text, err_dim);
	    fprintf(stderr, "Cannot execute regular expression on line %u: %s.\n", err_text, line);
	    exit(7);
	}
    }
    // ok reusing line this way is a little dirty but copying each string two times is worse, moreover...
    line[match[1].rm_eo] = 0; // ...there is always a '=' char after the match
    line[match[2].rm_eo] = 0; // ...there is always a '"' char after the match
    funct(line + match[1].rm_so, line + match[2].rm_so);
}

char * optarg; // getopt parameters

int main(const int ac, const char *av[]) {
    char * rc_conf_default = "/etc/defaults/rc.conf";
    char * rc_conf_current = "/etc/rc.conf";
    char * rc_conf_out = NULL;
    int no_comments = 0, full = 0, all = 0;
    regex_t re_default, re_current, * re_exceptions;
    FILE * in, * out = stdout;
    char line[MAX_LINE];
    struct element * el;
    int i, tabs;
    time_t now;
    unsigned int linenum;

    DEBUG_POINT_s(1, "parsing command-line")
    while ((i = getopt(ac, av, "d:c:w:anfeh?")) != -1)
	switch (i) {
	    case 'd': rc_conf_default = optarg; break;
	    case 'c': rc_conf_current = optarg; break;
	    case 'w': rc_conf_out = optarg; break;
	    case 'n': no_comments = 1; break;
	    case 'f': full = 1; break;
	    case 'a': all = 1; break;
	    case 'e':
		fprintf(stderr,
		    COPYRIGHT "\n"
		    "\n"
		    "Exceptions list:\n"
		);
		for(i = 0; i < NUM_EXCEPTIONS; i++)
		    fprintf(stderr, "%2d. /%s/ after '%s'\n", i + 1, exceptions[i].match, exceptions[i].previous);
		exit(1);
	    case 'h': // intentional fall-throughs
	    case '?': // undefined options get returned as '?' by getopt
	    default:  // help or unrecognized option, let's write usage and exit
		fprintf(stderr,
		    COPYRIGHT "\n"
		    "\n"
		    "Usage: %s [OPTION...]\n"
		    "    -d filename  default rc.conf file [%s]\n"
		    "    -c filename  current rc.conf file [%s]\n"
		    "    -w filename  write output to file\n"
		    "    -n           (no-comments) doesn't print comments with default value\n"
		    "    -f           (full) prints also options with default value\n"
		    "    -a           (all) prints all options already present in rc.conf\n"
		    "    -h           prints this help\n"
		    "    -e           prints exception list\n",
		    av[0], rc_conf_default, rc_conf_current
		);
		exit(1);
	}

    DEBUG_POINT_s(1, "compiling regular expressions")
    re_compile(&re_default, RE_DEFAULT, REG_EXTENDED);
    re_compile(&re_current, RE_CURRENT, REG_EXTENDED);
    for(i = 0; i < NUM_EXCEPTIONS; i++)
	re_compile(&exceptions[i].compiled, exceptions[i].match, REG_EXTENDED | REG_NOSUB);

    DEBUG_POINT_s(1, "parsing default file")
    in = file_open(rc_conf_default, "rt");
    linenum = 1;
    while(!feof(in))
	line_parse(&re_default, fgets(line, MAX_LINE, in), element_add, linenum++);
    fclose(in);

    DEBUG_POINT_s(1, "parsing config file")
    in = file_open(rc_conf_current, "rt");
    linenum = 1;
    while(!feof(in))
	line_parse(&re_current, fgets(line, MAX_LINE, in), element_change, linenum++);
    fclose(in);

    DEBUG_POINT_s(1, "generating output")
    if(rc_conf_out)
	out = file_open(rc_conf_out, "wt");
    time(&now);
    fprintf(out,
	"# Reordered by rclean on %s"
	"# " COPYRIGHT "\n"
	"# Please make all changes to this file, not to /etc/defaults/rc.conf\n"
	"# This file contains just the overrides from /etc/defaults/rc.conf\n",
	ctime(&now)
    );
    el = head;
    // elaborates the full list
    while(el) {
	// prints only if 'full' is activated or if value is different from default value
	if(full || ((el->value != el->value_def) && (all || (strcmp(el->value, el->value_def) != 0)))) {
	    fprintf(out, "%s=\"%s\"", el->key, el->value);
	    tabs = 52 - strlen(el->key) - strlen(el->value);
	    // print comment only if no 'no-comments' and (not 'full' or option is different from default)
	    if((!no_comments) && ((!full) || (strcmp(el->value, el->value_def) != 0))) {
		if(tabs >= 8) {
		    tabs >>= 3;
		    while(tabs--)
			fputc('\t', out);
		} else {
		    // at least a space if no tab is needed
		    fputc(' ', out);
		}
		fprintf(out, "# \"%s\"\n", el->value_def);
	    } else {
		fputc('\n', out);
	    }
	}
	el = el->next;
    }

    DEBUG_POINT_s(1, "exiting")
    exit(0);
}



syntax highlighted by Code2HTML, v. 0.9.1