//
//	Jim's C prototyping tool.  Copyright (c) 2003-2006 Jim Peters
//	<http://uazu.net/>.  Released under the GNU GPLv2 as published
//	by the Free Software Foundation.  See the file COPYING for
//	details, or visit <http://www.gnu.org/copyleft/gpl.html>.
//
//	Note that jcproto will generate nonsense output without
//	warning when the input is not valid C source.  Old-style K&R
//	function definitions are not handled.
//
//	This is a cut-down version of a C prototyping/building tool
//	called "jcpp".
//

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <dirent.h>

typedef struct OutBuf OutBuf;
typedef struct OutChunk OutChunk;

struct OutChunk {
   OutChunk *nxt;
   int len;
   char data[4];
};

struct OutBuf {
   OutChunk *list;
   int len;
};


// Prototypes
extern void usage();
extern void error(char *fmt, ...);
extern void warn(char *fmt, ...);
extern void *Alloc(int size);
extern void *StrDup(char *str);
extern void *MemDup(void *p0, void *p1);
extern int f_ext;
extern int f_stt;
extern char *wrk, *wrk_end;
extern int wrk_len;
extern int main(int ac, char **av);
extern void do_proto(int ac, char **av);
extern void gen_proto(OutBuf *ob, char *txt, int ext, int stt, char *fnam);
extern void dump(char *p, char *end);
extern struct stat sb;
extern int do_stat(char *fnam);
extern int is_dir(char *fnam);
extern int is_file(char *fnam);
extern int stat_time(char *fnam);
extern char *loadfile(char *fnam);
extern OutBuf *new_outbuf();
extern void del_outbuf(OutBuf *ob);
extern char *flush_outbuf(OutBuf *ob);
extern void obaddf(OutBuf *ob, char *fmt, ...);
extern void obadd(OutBuf *ob, char *str);


#define NL "\n"
void 
usage() {
   error("Jim's C prototyping tool.  Copyright 2003-2006 Jim Peters <http://uazu.net/>"
	 NL "Released under the GNU GPLv2; see file COPYING."
	 NL
	 NL "Usage: jcproto [options] <files...>"
	 NL "  Output to standard output prototypes for the functions and variables"
	 NL "  in the given .c files."
	 NL "Options:"
	 NL "  -e  Output only 'extern' declarations for non-static funcs/vars"
	 NL "  -s  Output only 'static' declarations for local functions/variables"
	 );
}
#undef NL

void 
error(char *fmt, ...) {
   va_list ap;
   va_start(ap, fmt);
   vfprintf(stderr, fmt, ap);
   fprintf(stderr, "\n");
   exit(1);
}

void 
warn(char *fmt, ...) {
   va_list ap;
   va_start(ap, fmt);
   vfprintf(stderr, fmt, ap);
   fprintf(stderr, "\n");
}

void *
Alloc(int size) {
   void *vp= calloc(1, size);
   if (!vp) error("Out of memory");
   return vp;
}

void *
StrDup(char *str) {
   char *cp= strdup(str);
   if (!cp) error("Out of memory");
   return cp;
}

void *
MemDup(void *p0, void *p1) {
   int size= (char*)p1-(char*)p0;
   char *cp= malloc(size);
   if (!cp) error("Out of memory");
   memcpy(cp, p0, size);
   return cp;
}

#define ALLOC(type) ((type*)Alloc(sizeof(type)))
#define ALLOC_ARR(cnt, type) ((type*)Alloc((cnt) * sizeof(type)))


//
//	Globals
//

int f_ext= 0;
int f_stt= 0;
char *wrk, *wrk_end;	// Scratch work buffer
int wrk_len;		// Scratch buffer size (32K)


//
//	Main
//

int 
main(int ac, char **av) {
   char ch, *p;

   wrk= Alloc(wrk_len= 32768);
   wrk_end= wrk + wrk_len;

   ac--; av++;

   while (ac && av[0][0] == '-') {
      p= 1 + *av++; ac--;
      while ((ch= *p++)) switch (ch) {
       case 'e': f_ext= 1; break;
       case 's': f_stt= 1; break;
       default: usage(); break;
      }
   }

   if (ac < 1) usage();
   do_proto(ac, av);
   return 0;
}


//
//	Prototyping mode
//

void 
do_proto(int ac, char **av) {
   OutBuf *ob= new_outbuf();
   
   while (ac-- > 0) {
      char *fnam= *av++;
      char *txt= loadfile(fnam);
      char *p= strchr(fnam, 0) - 3;
      char *body= txt;

      obaddf(ob, "/* file: %s */\n", fnam);
      gen_proto(ob, body, !f_stt, !f_ext, fnam);
      obadd(ob, "\n");
      
      free(txt);
   }

   {
      char *tmp= flush_outbuf(ob);
      fputs(tmp, stdout);
      free(tmp);
      del_outbuf(ob);
   }
}


//
//	Generate prototypes to the given buffer.  'ext' is 1 to
//	include extern declarations of non-static functions and
//	variables.  'stt' is 1 to include local prototypes of static
//	functions.
//

void 
gen_proto(OutBuf *ob, char *txt, int ext, int stt, char *fnam) {
   char *wp= wrk;
   char *rp= txt;
   char ch, *p;
   OutBuf *obtmp= new_outbuf();
   char *txt2;	
      
   // Make a copy of 'txt', but stripping out the contents of all ""
   // and '' strings, removing all macro definitions, removing all
   // comments, reducing white space to single NUL characters, and
   // inserting NULs between all tokens relevant to top-level
   // declarations and definitions.
   {
      int in_comm1= 0;	// Within /* */ comments ?
      int in_comm2= 0;	// Within //... comments ?
      int in_str= 0;	// Within a "" or '' quoted string?
      int in_macro= 0;	// Within a macro definition

      *wp++= ' ';

#define ADDWS if (wp[-1] != ' ') *wp++= ' '
      
      while (ch= *rp++) {
	 if (wp + 1024 > wrk_end) {
	    if (wp >= wrk_end) error("Work buffer overflow in gen_proto()");
	    *wp++= 0; obadd(obtmp, wrk);
	    wp= wrk; *wp++= ' ';
	 }
	 
	 if (in_macro) {
	    if (ch == '\\') {
	       while (rp[0] == ' ' || rp[0] == '\t') rp++;
	       if (rp[0] == '\n') rp++;
	       continue;
	    }
	    if (ch == '\n') in_macro= 0;
	    continue;
	 }
	 
	 if (in_comm1) {
	    if (ch == '*' && rp[0] == '/') {
	       rp++; in_comm1= 0; continue;
	    }
	    continue;
	 }
	 
	 if (in_comm2) {
	    if (ch == '\n') in_comm2= 0; 
	    continue;
	 }
	 
	 if (in_str) {
	    if (ch == '\\' && rp[0]) rp++; 
	    if (ch == in_str) in_str= 0; 
	    continue;
	 }
	 
	 // Macro is recognised as \n[ \t]*#[ \t]*[a-zA-Z]
	 if (ch == '#') {
	    for (p= rp-1; p > txt && (p[-1] == ' ' || p[-1] == '\t'); --p) ;
	    if (p == txt || (p > txt && p[-1] == '\n')) {
	       while (rp[0] == ' ' || rp[0] == '\t') rp++;
	       if (isalpha(rp[0])) {
		  in_macro= 1;
		  continue;
	       }
	    }
	 }
	 
	 if (ch == '/' && rp[0] == '*') {
	    ADDWS; rp++; in_comm1= 1; 
	    continue;
	 }
	 if (ch == '/' && rp[0] == '/') {
	    ADDWS; rp++; in_comm2= 1; 
	    continue;
	 }
	 if (ch == '\'' || ch == '\"') {
	    ADDWS; *wp++= ch; *wp++= ch; *wp++= ' ';
	    in_str= ch;
	    continue;
	 }
	 if (isspace(ch)) {
	    ADDWS;
	    continue;
	 }
	 if (isalpha(ch) || ch == '_') {
	    ADDWS;
	    *wp++= ch;
	    while (isalnum(*rp) || *rp == '_') *wp++= *rp++;
	    *wp++= ' ';
	    continue;
	 }
	 if (strchr("[](){}&*;,=", ch)) {
	    ADDWS; *wp++= ch; *wp++= ' ';
	    continue;
	 }
	 
	 *wp++= ch;
      }

      ADDWS;  // Make sure there is a trailing ' '
      *wp= 0; obadd(obtmp, wrk);
      
      txt2= flush_outbuf(obtmp);
      del_outbuf(obtmp); 
      obtmp= 0;

      // Convert all the spaces to NULs and lose any dups (possible
      // because of joining chunks) and add a final end-marker NUL
      for (rp= txt2+1, wp= txt2; ch= *rp++; ) {
	 if (ch == ' ') { ch= 0; while (*rp == ' ') rp++; }
	 *wp++= ch;
      }
      *wp++= 0;
      
#undef ADDWS
   }

   // Work through 'txt2', replacing function-def {} contents with ';'
   // and eliminating assignments ("= ... ," or "= ... ;").  Note that
   // struct/union definition {} contents are left alone.  Also
   // eliminates the contents of the first [] of each definition,
   // i.e. first [] after each ';' or ','
   {
      int tok= 0;	// Token count
      int su_tok= -9;	// Token number of last struct/union keyword
      int blev= 0;	// Brace level
      int fsb_flag= 1;	// First-square-bracket flag

      rp= wp= txt2;
      while (*rp) {
	 tok++;
	 if (0 == strcmp(rp, "struct") ||
	     0 == strcmp(rp, "union")) 
	    su_tok= tok;
	 
	 if (*rp == '{' && blev == 0 && su_tok < tok-2) {
	    // Skip this entire {} and replace with a ';' unless it is a struct/union def
	    blev++; while (*rp++) ;
	    while (blev && *rp) {
	       if (*rp == '{') blev++;
	       if (*rp == '}') blev--;
	       while (*rp++) ;
	    }
	    if (blev) error("Missing closing {} braces at end of file: %s", fnam);
	    *wp++= ';'; *wp++= 0;
	    fsb_flag= 1;
	    continue;
	 }

	 if (*rp == '[' && fsb_flag && blev == 0) {
	    // Skip everything until next ']' at this level, but only for first [] of def
	    int lev= 0;
	    fsb_flag= 0;
	    while ((ch= *rp)) {
	       if (!rp[1]) {
		  if (ch == '(' || ch == '{' || ch == '[') lev++;
		  else if (ch == ')' || ch == '}' || ch == ']') lev--;
		  if (lev < 0) error("Too many closing () or {} or [] at end of file: %s", fnam);
		  if (lev == 0) break;
	       }
	       while (*rp++) ;
	    }
	    if (lev) error("Missing closing () or {} or [] at end of file: %s", fnam);
	    *wp++= '['; *wp++= 0;
	    continue;
	 }
	    
	 if (*rp == '{') blev++;
	 if (*rp == '}') blev--;
	 
	 if (*rp == '=') {
	    // Skip assignment up to next ',' or ';' at this level
	    int lev= 0;
	    while ((ch= *rp)) {
	       if (!rp[1]) {
		  if (ch == '(' || ch == '{') lev++;
		  else if (ch == ')' || ch == '}') lev--;
		  if (lev < 0) error("Too many closing () or {} at end of file: %s", fnam);
		  if (lev == 0 && (ch == ',' || ch == ';')) break;
	       }
	       while (*rp++) ;
	    }
	    if (lev) error("Missing closing () or {} at end of file: %s", fnam);
	    // Drops through ...
	 }

	 // Watch out for ',' and ';'
	 if (rp[0] == ',' || rp[0] == ';') 
	    fsb_flag= 1;
	 
	 // Copy this token over
	 while (*wp++= *rp++) ;
      }
      *wp++= 0;
      
      if (blev > 0) error("Missing closing {} braces at end of file: %s", fnam);
      if (blev < 0) error("Too many closing {} braces at end of file: %s", fnam);
   }

   // Okay, now we have each declaration terminated by a ';' token.
   // Take them one by one and see what needs to be done.
   rp= txt2;
   while (*rp) {
      char *rp0= rp;
      int f_extern= 0;
      int f_static= 0;
      int f_inline= 0;
      int f_typedef= 0;
      int f_func= 0;
      int f_knr= 1;
      int f_lone_su= 0;	// struct/union def without variable/func attached
      int lev= 0;

      // Find end of declaration and check for important keywords and
      // other features
      while (*rp && !(*rp == ';' && lev == 0)) {
	 int id= (isalpha(rp[0]) || rp[0] == '_');
	 int clbr= (rp[0] == '}');
	 int l0clpa= (lev == 0 && rp[0] == ')');
	 if (id && lev == 0) {
	    if (0 == strcmp(rp, "extern")) f_extern= 1;
	    if (0 == strcmp(rp, "static")) f_static= 1;
	    if (0 == strcmp(rp, "typedef")) f_typedef= 1;
	    if (0 == strcmp(rp, "inline")) f_inline= 1;
	 } else {
	    if (rp[0] == '{') lev++;
	    if (rp[0] == '}') lev--;
	 }
	 while (*rp++) ;
	 if (id && rp[0] == '(') f_func= 1;	// Function definition
	 if (clbr && lev == 0 && rp[0] == ';') f_lone_su= 1; 	// Lone struct/union def
	 if (l0clpa && rp[0] == ';') f_knr= 0;	// Normal function def
      }
      if (!*rp) error("Incomplete definition/declaration at end of file: %s", fnam);
      while (*rp++) ;

      if (f_extern) continue;	// Ignore all 'extern' definitions
      if (f_lone_su) continue;	// Ignore all struct/union defs; they should be in headers
      if (f_typedef) continue;	// Ignore all typedefs; they should be in headers
      if (f_static && !stt) continue;
      if (!f_static && !ext) continue;
      if (f_static && !f_func) continue;	// Static variables; hard to pre-declare
      if (f_func && f_knr) {
	 warn("Old-style K&R function definitions not handled:");
	 fprintf(stderr, "  "); dump(rp0, rp); fprintf(stderr, "\n");
	 exit(1);
      }

      if (ext && !f_static) 
	 obadd(ob, "extern ");

      wp= wrk; *wp++= ' ';
      while (rp0 < rp) {
	 int an0= isalnum(wp[-1]) || wp[-1] == '_';
	 int an1= isalnum(*rp0) || *rp0 == '_';
	 if (an0 && (an1 || strchr("*{", *rp0))) *wp++= ' ';
	 if (strchr(",;{}", wp[-1])) *wp++= ' ';
	 while (*wp= *rp0++) wp++;
      }
      *wp++= '\n';
      *wp++= 0;
      if (wp >= wrk_end) error("Work buffer overflow in gen_proto()");
      obadd(ob, wrk+1);
   }

   free(txt2);
}


//
//	Dump out a string to STDERR, mapping NUL to space
//

void 
dump(char *p, char *end) {
   for (; p < end; p++)
      fputc(*p ? *p : ' ', stderr);
}


//
//	File status-testing code
//

struct stat sb;

// Returns 1 okay, 0 doesn't exist
int 
do_stat(char *fnam) {
   int rv= stat(fnam, &sb);
   if (rv < 0) switch (errno) {
    case ENOENT:
    case ENOTDIR:
       return 0;
    case ELOOP:
       error("Symbolic link loop in path:\n %s", fnam);
    case EACCES:
       error("Permission denied to path:\n %s", fnam);
    default:
       error("Error %s accessing path:\n %s", strerror(errno), fnam);
   }
   return 1;
}

int 
is_dir(char *fnam) {
   return do_stat(fnam) ? S_ISDIR(sb.st_mode) : 0;
}

int 
is_file(char *fnam) {
   return do_stat(fnam) ? S_ISREG(sb.st_mode) : 0;
}

// Returns timestamp on file, or 0 if doesn't exist
int 
stat_time(char *fnam) {
   return do_stat(fnam) ? sb.st_mtime : 0;
}

   
//
//	Load up a file into memory; returns allocated block of memory,
//	with NUL-termination.
//

char *
loadfile(char *fnam) {
   int len, cnt;
   char *rv;
   FILE *in;
   char *p;

   if (!do_stat(fnam)) 
      error("File not found:\n %s", fnam);

   len= sb.st_size;
   rv= Alloc(len+1);
   
   if (!(in= fopen(fnam, "r"))) 
      error("Can't open file:\n %s", fnam);
   if (len && 1 != fread(rv, len, 1, in))
      error("Read error on file:\n %s", fnam);
   if (0 != fclose(in))
      error("Read error on file:\n %s", fnam);

   cnt= 0;
   for (p= rv; len-- > 0; p++) if (*p == 0) { *p= ' '; cnt++; }
   if (cnt > 0)
      warn("File contains NUL characters which have been replaced by spaces:\n %s", fnam);

   return rv;
}


//
//	Output buffer chain handling
//

OutBuf *
new_outbuf() {
   OutBuf *rv= ALLOC(OutBuf);
   return rv;
}

//
//	Delete output buffer
//

void 
del_outbuf(OutBuf *ob) {
   while (ob->list) { OutChunk *oc= ob->list; ob->list= oc->nxt; free(oc); }
   free(ob);
}

//
//	Merge all the sections into one allocated block of memory and
//	return it.
//

char *
flush_outbuf(OutBuf *ob) {
   char *rv= Alloc(ob->len + 1);
   char *p= rv + ob->len;
   OutChunk *oc;

   while (ob->list) {
      oc= ob->list; ob->list= oc->nxt;
      p -= oc->len;
      memcpy(p, oc->data, oc->len);
      free(oc);
   }
   ob->len= 0;

   if (p != rv) 
      error("Internal error in OutBuf code: offset of %d found in flush_outbuf()", p-rv);
   return rv;
}

//
//	Add a formatted string to the end of the output buffer chain.
//
   
void 
obaddf(OutBuf *ob, char *fmt, ...) {
   int len;
   char buf[4096];
   OutChunk *oc;
   va_list ap;
   va_start(ap, fmt);
   len= vsnprintf(buf, sizeof(buf), fmt, ap);
   if (len < 0 || len+1 > sizeof(buf))
      error("obaddf() buffer overflow; formatting string:\n %s", fmt);
   
   oc= (void*)Alloc(sizeof(OutChunk) - 4 + len + 1);
   memcpy(oc->data, buf, len);
   oc->len= len;
   oc->nxt= ob->list;
   ob->len += len;
   ob->list= oc;
}

//
//	Add a string to the end of the output buffer chain.
//
   
void 
obadd(OutBuf *ob, char *str) {
   int len;
   OutChunk *oc;
   len= strlen(str);
   oc= (void*)Alloc(sizeof(OutChunk) - 4 + len + 1);
   memcpy(oc->data, str, len);
   oc->len= len;
   oc->nxt= ob->list;
   ob->len += len;
   ob->list= oc;
}


// END //
