/*

   snowdrop - text watermarking and watermark recovery
   ---------------------------------------------------
 
   Copyright (C) 2002 by Michal Zalewski <lcamtuf@coredump.cx>
 
   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.

   As a special exception, this program may be linked with the
   OpenSSL library, despite that library's more restrictive license.

   English language backend.

*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>

#ifdef USE_OPENSSL
#include <openssl/md5.h>
#else
#include <md5global.h>
#include <md5.h>
#define MD5_Init   MD5Init
#define MD5_Final  MD5Final
#define MD5_Update MD5Update
#endif /* USE_OPENSSL */

#include "language.h"

// Max synonym cache entries
#define MAXCACHE	4096
#define MAXSYN		16

// How often do you want typos (number of atoms)?

#ifndef ENG_FINE 
#define TYPORATIO	150
#else
#define TYPORATIO	500
#endif

static int   word_cnt;		// Original term counter
static char* use_quot;		// Close quotes using this string
static int   indent_val=-1;	// Indentation, if any
static char  prev_punct;	// Previous atom was a punctuation mark
static int   cur_size;		// Current input atom storage capacity
static int   cur_mod;		// Current input atom modification
static int   just_testing;	// Just teestiiing!

#define MOD_NONE	0	// No modification
#define MOD_SYNONYM	1	// Put a synonym
#define MOD_TYPO	2  	// Make a typo
#define MOD_QUOTE	4	// Change quotes
#define MOD_PSPACE	5	// Add spaces
#define MOD_CAPS	7	// Capitalization
#define MOD_PERIOD	8	// ; -> .
#define MOD_DASH	9	// - -> 0xad

#define MAXWORD 1024


struct syncache {
  char* from;
  char* to[MAXSYN+1];
  char tcnt;
};

struct syncache scache[MAXCACHE+1];
int sctop;

struct sd_syns { char *from, *to, bid; };
static struct sd_syns syn[MAXWORD+1];


// Load synonym database, of course...
static void load_synonyms(void) {
  int line=0,added=0,z,m;
  char buf[MAXBUF+1];
  FILE* f;

  if (syn[0].from) return;

  if (getenv("SD_SYNONYMS")) {
    strcpy(buf,getenv("SD_SYNONYMS"));
    f=fopen(buf,"r");
  } else {
    sprintf(buf,"%s/.snowdrop/synonyms",getenv("HOME"));
    f=fopen(buf,"r");
    if (!f) f=fopen("/usr/share/snowdrop/synonyms","r");
    if (!f) f=fopen("synonyms","r");
  }
  if (!f) fatal("cannot find synonym dictionary (%s)",buf);

  while (fgets(buf,MAXBUF,f)) {
    char* bcop;
    char w1[MAXBUF], w2[MAXBUF],c;
    int i;
    line++;
    if (buf[strlen(buf)-1]=='\n') buf[strlen(buf)-1]=0;
    if (strchr(buf,'#')) *strchr(buf,'#')=0;
    bcop=buf;
    while (isspace(*bcop)) bcop++;
    if (!(*bcop)) continue;
    if (sscanf(buf,"%s %c %s",w1,&c,w2)!=3)
      fatal("malformed dictionary line %d [1]",line);

    if (!strcasecmp(w1,w2)) fatal("NOOP dictionary entry at line %d",line);

    i=0;
    while (syn[i].from) {

      if (!strcasecmp(syn[i].from,w1))
        if (!strcasecmp(syn[i].to,w2))
          fatal("duplicate dictionary entry for %s - %s at line %d",w1,w2,line);

      if (!strcasecmp(syn[i].from,w2))
        if (!strcasecmp(syn[i].to,w1))
          fatal("duplicate dictionary entry for %s - %s (reverse) at line %d",w1,w2,line);

      i++;

    }

    for (z=0;z<strlen(w1);z++) if (isupper(w1[z])) fatal("line %d: uppercase entry (%s)",line,w1);
    for (z=0;z<strlen(w2);z++) if (isupper(w2[z])) fatal("line %d: uppercase entry (%s)",line,w2);

    syn[i].from=strdup(w1);
    if (!syn[i].from) fatal("not enough memory");
    syn[i].to=strdup(w2);
    if (!syn[i].to) fatal("not enough memory");

    if (c=='>') syn[i].bid=0; else
    if (c=='|') syn[i].bid=1; else
      fatal("malformed dictionary line %d [2]",line);
    added++;

    // Add a synonym for w1 -> w2

    for (m=0;m<sctop;m++) if (!strcmp(scache[i].from,w1)) break;
    if (m==sctop) sctop++;
    if (sctop>=MAXCACHE) fatal("MAXCACHE exceeded");

    scache[m].from=syn[i].from;
    scache[m].to[(int)scache[m].tcnt]=syn[i].to;
    scache[m].tcnt++;
    if (scache[m].tcnt>=MAXSYN) fatal("MAXSYN for %s exceeded",w1);

    // Add a synonym for w2 -> w1 if bid

    if (syn[i].bid) {
      for (m=0;m<sctop;m++) if (!strcmp(scache[i].from,w2)) break;
      if (m==sctop) sctop++;
      if (sctop>=MAXCACHE) fatal("MAXCACHE exceeded");
  
      scache[m].from=syn[i].to;
      scache[m].to[(int)scache[m].tcnt]=syn[i].from;
      scache[m].tcnt++;
      if (scache[m].tcnt>=MAXSYN) fatal("MAXSYN for %s exceeded",w2);
      }

  }

  fclose(f);
  debug("[+] Loaded %d synonyms (%d lines parsed).\n",added,line);

}

static unsigned int got_md5;

unsigned int md5_importantstuff(void) {
  int i=0;
  unsigned int result[4];
  MD5_CTX kuku;
  load_synonyms();
  if (got_md5) return got_md5;
  MD5_Init(&kuku);
  while (syn[i].from) {
    MD5_Update(&kuku,&i,sizeof(int));
    MD5_Update(&kuku,syn[i].from,strlen(syn[i].from)+1);
    MD5_Update(&kuku,&syn[i].bid,1);
    MD5_Update(&kuku,syn[i].to,strlen(syn[i].to)+1);
    MD5_Update(&kuku,"-|-",3);
    i++;
  }
  MD5_Final((char*)result,&kuku);
  
  return got_md5=(result[0] ^ result[1] ^ result[2] ^ result[3]);
}



// Get the number of synonyms matching the term.
static int lookup_syn_cnt(const char* term) {
  int q=0;
  while (q<sctop) {
    if (!strcasecmp(scache[q].from,term)) return scache[q].tcnt;
    q++;
  }
  return 0;
}


static char caps[MAXBUF+1];


// Try to copy the capitalization scheme from original term...
// Assume that first letter is uppercase or all letters are uppercase.
// This if for compatibility with MOD_CAPS.
static char* handle_caps(const char* orig,const char* nterm) {
  int q=0;

  if (!nterm[0]) fatal("handle_caps with empty nterm");

  strcpy(caps,nterm);

  if (isupper(orig[0])) caps[0]=toupper(caps[0]);

  if (isupper(orig[1]) && orig[2])
    while (caps[q]) { caps[q]=toupper(caps[q]); q++; }

  return caps;
}


// Ok, give me the actual num-th synonym for 'term'.
static char* lookup_syn_no(const char* term,const int num) {
  int q=0;
  while (q<sctop) {
    if (!strcasecmp(scache[q].from,term)) return handle_caps(term,scache[q].to[num]);
    q++;
  }
  return 0;
}


static const char* input_data;
static int input_off;
static const char* water_data;
static int water_off;

static int cur_punct;
static int linesofar;  // How many things in this line?
static int atomsofar; // How many atoms in this line?
char resetinnext;

void  set_original(const char* buf) {
  if (!buf) fatal("set_original(NULL)");
  input_data=buf;
  input_off=0;
  water_off=0;
  load_synonyms();

  linesofar=0;
  atomsofar=0;
  resetinnext=0;
  just_testing=0;
  word_cnt=0;
  cur_punct=0;
  use_quot=0;
  indent_val=-1;
  prev_punct=0;
  cur_size=0;
  cur_mod=0;
}


void  set_watermarked(const char* buf) {
  if (!buf) fatal("set_watermarked(NULL)");
  if (!input_data) fatal("set_watermarked before set_original");
  water_data=buf;
  water_off=0;
}


char FOOBAR[]=".f00.b4r.";

static char orig_buf[MAXBUF+1];


char* get_orig_atom(void) {
  char* now=(char*)input_data+input_off, sth[2];
  unsigned char or;
  char* nospaced;
  if (!input_data) fatal("get_orig_atom before set_original");

  if (!*now) return 0;

  word_cnt++;

  prev_punct=cur_punct;

  if (resetinnext) { linesofar=0; atomsofar=0; resetinnext=0; }

  orig_buf[0]=0;
  // Duplicate spaces
  while (*now == ' ' || *now == '\t') {
#ifdef ENG_FINE
    strcat(orig_buf,*now==' '?" ":"\t");
#else
    strcat(orig_buf,*now==' '?" ":"        ");
#endif
    now++; 
  }
  nospaced=orig_buf+strlen(orig_buf);
  // Append next one character unconditionally.
  or=sth[0]=*now;
  sth[1]=0;
  strcat(orig_buf,sth);
  now++;
  // Certain chars should be kept together...
  if (!(or=='\'' || or=='`' || or==',')) or=0xff;
  if (or==0xff && !isalnum(sth[0])) or=0xfe;

contorig:

  // Copy the rest. Stop on something that does not belong.
  while ((or==0xff && isalnum(*now)) || (*now==or)) { 
    sth[0]=*now; 
    strcat(orig_buf,sth); 
    now++; 
  }

  // If we stopped because of ' surrounded by chars, continue.
  if (isalnum(*(now-1)) && *now=='\'' && isalnum(*(now+1))) {
    sth[0]=*now; 
    strcat(orig_buf,sth); 
    now++; 
    goto contorig;
  }   

  input_off=now-input_data;
  if (!*now) { if (!strlen(orig_buf)) return 0; }

  if (use_quot==FOOBAR) use_quot=0;
  if (use_quot && (*nospaced=='\'' || *nospaced=='"')) {
    int q=strlen(nospaced);
    strcpy(nospaced,use_quot);
    linesofar-=strlen(nospaced)-q;
    use_quot=FOOBAR;
  }

  if (indent_val!=-1) {
    if (orig_buf[0]==' ' && !linesofar) {
      char tmp[MAXBUF+1];
      int i;
      tmp[0]=0;
      for (i=0;i<indent_val;i++) strcat(tmp," ");
      strcat(tmp,nospaced);
      strcpy(orig_buf,tmp);
      linesofar-=indent_val;
    }
  }

  cur_punct=!isalnum(*nospaced);
  if (*nospaced=='\n') cur_punct=0;

  if (strchr(orig_buf,'\n')) resetinnext=1;
  else {
    linesofar+=strlen(orig_buf); 
    atomsofar++;
  }

  // Too late. Paragraph ended. You die.
  if (!linesofar && strchr(orig_buf,'\n')) {
    indent_val=-1; use_quot=0;
  }

  return orig_buf;
}


static char water_buf[MAXBUF+1];

char* get_water_atom(void) {
  char* now=(char*)water_data+water_off, sth[2];
  unsigned char or;
  char* nospaced;
  if (!water_data) fatal("get_water_atom before set_watermarked");
  if (!*now) return 0;

  water_buf[0]=0;
  // Duplicate spaces
  while (*now == ' ' || *now == '\t') {
    strcat(water_buf,*now==' '?" ":"\t");
    now++; 
  }

  nospaced=water_buf+strlen(water_buf);
  // Append next one character unconditionally.
  or=sth[0]=*now;
  sth[1]=0;
  strcat(water_buf,sth);
  now++;
  // Certain chars should be kept together...
  if (!(or=='\'' || or=='`' || or==',')) or=0xff;
  if (or==0xff && !isalnum(sth[0])) or=0xfe;

contwater:

  // Copy the rest. Stop on something that does not belong.
  while ((or==0xff && isalnum(*now)) || (*now==or)) { 
    sth[0]=*now; 
    strcat(water_buf,sth); 
    now++; 
  }

  // If we stopped because of ' surrounded by chars, continue.
  if (isalnum(*(now-1)) && *now=='\'' && isalnum(*(now+1))) {
    sth[0]=*now; 
    strcat(water_buf,sth); 
    now++; 
    goto contwater;
  }   

  water_off=now-water_data;
  if (!*now) { if (!strlen(water_buf)) return 0; }

  return water_buf;
}



#define CHECK_STOR(siz,id) if ((siz) >= top_storage) { \
                             mod_type=(id); top_storage=(siz); }


static inline int storcap(int max) {
  int bits=0,pw=1;
  while (max >= pw) {
    pw<<=1;
    bits++;
  }
  return bits-1;
}


static int mod_type;

int   get_storage(const char* orig, const int domain) {
  const char* text=orig;
  int tsp=0;
  int top_storage=0;

  if (!orig) fatal("get_storage(NULL...)");
  while (*text == ' ') {tsp++; text++; }

  switch (domain) {
    case DOMAIN_WHITE:

#ifndef ENG_FINE
      if (prev_punct && orig[0]==' ') { 
        CHECK_STOR(1,MOD_PSPACE);
      }
#endif

      if (strchr(orig,'\n')) {
#ifdef ENG_FINE
        if (linesofar>81) { CHECK_STOR(1,MOD_PSPACE); } else
        if (linesofar<76) CHECK_STOR(1,MOD_PSPACE);
#else
        if (linesofar>81) { CHECK_STOR(4,MOD_PSPACE); } else
        if (linesofar<79) CHECK_STOR(storcap(79-linesofar),MOD_PSPACE);
#endif
      }
 
      break;

    case DOMAIN_GRAMMAR:

      // FIXME: make typos a bit less predictable?
      if (!(word_cnt % TYPORATIO) && isalnum(*text)) {
#ifdef ENG_FINE
        CHECK_STOR(4,MOD_TYPO);
#else
        CHECK_STOR(5,MOD_TYPO);
#endif
      }

      break;

    case DOMAIN_FORMAT:

      // If the word has at least two uppercase letters,
      // either make it first-only or all uppercase. This gives us
      // one bit and would not break synonym capitalization.
      { int i,gu=0;
        for (i=0;i<strlen(text);i++) {
          if (isupper(text[i])) gu++;
        }
        if (gu > 1) CHECK_STOR(1,MOD_CAPS);
      }

#ifndef ENG_FINE
      // We can ruin some ;s ;-)
      if (*text==';') CHECK_STOR(1,MOD_PERIOD);
#endif

      // We can ruin some -s ;-)
      if (*text=='-' || *(unsigned char*)text==0xad) CHECK_STOR(1,MOD_DASH);

      // We can also mess with quotes. This is good.
      if (!use_quot)   
        if (!strcmp(text,"''") || !strcmp(text,"`") || !strcmp(text,"'") ||
            !strcmp(text,"``") || !strcmp(text,"\"") || !strcmp(text,",,")) CHECK_STOR(3,MOD_QUOTE);

      break;

    case DOMAIN_SYNONYMS:
      // Determine how many synonyms can be substituted for a word.
      // If any, add 1 to the number (as we can left the word unchanged,
      // as well). Now, determine largest power of two less or equal to
      // the number we got. This is our storage capacity.

      { int i;
        i=lookup_syn_cnt(text);
        if (i>0) {
          CHECK_STOR(storcap(i+1),MOD_SYNONYM);
        }
      }
      break;

    default: fatal("bogus domain in get_storage");

  }

  return top_storage;

}

static char setv[MAXBUF+1];

static char typovals[]="abcdefghijklmnopqrstuvwxyz0123456789";


char* set_value(const char* orig,int value, const int domain) {
  int cap,i;
  const char* text=orig;
  char* foo;
  cap=get_storage(orig,domain);
  if (cap <= 0) fatal("set_value with fixed atom");
  if (cap > 16) fatal("set_value with atom of excessive storage capacity");
  if (value >= (1<<cap)) fatal("set_value: new value exceeds storage capacity");
  if (value < 0) fatal("set_value: new value less than zero");

  setv[0]=0;
  while (*text == ' ') text++;

  switch (mod_type) {

    case MOD_PSPACE:

      // "Bite my shiny metal ass!"
      for (i=0;i<value+1;i++) strcat(setv," "); 
      strcat(setv,text);
      return setv;

    case MOD_TYPO:

        // There is a rare potential glitch here when capitalization
        // changes are ruined by digits. Should happen rarely, fix it
        // some day.
        strcpy(setv,orig);
        i=word_cnt % strlen(text);
        if (isupper(setv[text-orig+i]))
        setv[text-orig+i]=toupper(typovals[value]);
        else
        setv[text-orig+i]=typovals[value];

      return setv;

    case MOD_CAPS:
      strcpy(setv,orig);
      for (i=0;i<strlen(setv);i++) setv[i]=value?tolower(setv[i]):toupper(setv[i]);
      setv[text-orig]=toupper(setv[text-orig]);
      return setv;

    case MOD_PERIOD:
      strcpy(setv,orig);
      *strchr(setv,';')=value?';':'.';
      return setv;

    case MOD_DASH:
      strcpy(setv,orig);
      foo=strchr(setv,'-');
      if (!foo) foo=strchr(setv,0xad);
      *foo=value?'-':0xad;
      return setv;

    case MOD_QUOTE:
      i=0;
      while (orig[i]==' ') { strcat(setv," "); i++; }
      switch (value) {
         case 0: strcat(setv,"``"); if (!just_testing) use_quot="''"; break;
         case 1: strcat(setv,"''"); if (!just_testing) use_quot="''"; break;
         case 2: strcat(setv,"'");  if (!just_testing) use_quot="'"; break;
         case 3: strcat(setv,"`");  if (!just_testing) use_quot="'"; break;
         case 4: strcat(setv,"\""); if (!just_testing) use_quot="\""; break;
         case 5: strcat(setv,",,"); if (!just_testing) use_quot="''"; break;
         case 6: strcat(setv,"\xb4"); if (!just_testing) use_quot="\xb4"; break;
         case 7: strcat(setv,"\xbd"); if (!just_testing) use_quot="\xbd"; break;
         default: fatal("gremlins in the keyboard");
      }
      return setv;

    case MOD_SYNONYM:

      i=0;
      while (orig[i]==' ') { strcat(setv," "); i++; }
      if (!value) { strcat(setv,text); return setv; }
      strcat(setv,lookup_syn_no(text,value-1));
      return setv;

    default: fatal("bogus mod_type in set_value");

  }

  return setv;

}


char setval_copy[MAXBUF+1];


// Mooom! This is worse than terrible!
int strspcmp(const char* a,const char* b) {
  while (isspace(*a)) a++;
  while (isspace(*b)) b++;
  if (!*a || !*b) return 31337; // Bleh.
  return strcmp(a,b);
}


int strspcasecmp(const char* a,const char* b) {
  while (isspace(*a)) a++;
  while (isspace(*b)) b++;
  if (!*a || !*b) return 31337; // Bleh.
  return strcasecmp(a,b);
}


static int warned;

int get_value(const char* orig,const char* water,int* scr,int* va,char test) {
  int i3,i2,i1,i0;
  int sc[4];
  int cnt=0;

  if (!orig) fatal("get_value with orig==NULL");
  if (!water) fatal("get_value with water==NULL");
  if (!scr) fatal("get_value with sc==NULL");
  if (!va) fatal("get_value with va==NULL");

redome:

  // Enter dummy mode, save buffer.
  just_testing=1;

  sc[3]=get_storage(orig,3);
  sc[2]=get_storage(orig,2);
  sc[1]=get_storage(orig,1);
  sc[0]=get_storage(orig,0);

  for (i3=0;i3<(1<<sc[3]);i3++) {
    char b3[MAXBUF+1];    
    
    if (!sc[3]) strcpy(b3,orig); else strcpy(b3,set_value(orig,i3,3));

    for (i2=0;i2<(1<<sc[2]);i2++) {
      char b2[MAXBUF+1];    
      if (!sc[2]) strcpy(b2,b3); else strcpy(b2,set_value(b3,i2,2));

      for (i1=0;i1<(1<<sc[1]);i1++) {
        char b1[MAXBUF+1];    
        if (!sc[1]) strcpy(b1,b2); else strcpy(b1,set_value(b2,i1,1));

        for (i0=0;i0<(1<<sc[0]);i0++) {
          char b0[MAXBUF+1];    
          if (!sc[0]) strcpy(b0,b1); else strcpy(b0,set_value(b1,i0,0));
          // If final result matches...
          // First loop: just compare
          // Second loop: without spaces; compare with strnspcmp
          // Third loop: without spaces and grammar; compare with strnspcmp
          // Fourth loop: without spaces, grammar and notation; strnspcase...
          // Note that strnspcmp and strnspcasecmp return 0 if no non-space
          // characters are found to avoid stupid problems.

          if ((cnt==0 && !strcmp(water,b0)) ||
              (cnt==1 && !strspcmp(water,b1)) ||
              (cnt==2 && !strspcmp(water,b2)) ||
              (cnt==3 && !strspcasecmp(water,b3))) {

            just_testing=0;

            if (!test) {
              strcpy(setval_copy,setv);
              // Commit.
              if (sc[3]) set_value(orig,i3,3);
              if (sc[2]) set_value(orig,i2,2);
              if (sc[1]) set_value(orig,i1,1);
              if (sc[0]) set_value(orig,i0,0);
              strcpy(setv,setval_copy);
            }
            // Restore buffer...
            // Write capacities and values back...
            *(scr++)=sc[0]; *(va++)=i0;
            *(scr++)=sc[1]; *(va++)=i1;
            *(scr++)=sc[2]; *(va++)=i2;
            *(scr++)=sc[3]; *(va++)=i3;
            if (cnt && !warned) {
//              debug("[!] Data missing in certain channels (spell-checked or reformatted file?).\n");
              warned=1;
            }
            return 1;
	  }
        }
      }
    }
  }

  cnt++;
  if (cnt<=3) goto redome;

  strcpy(setval_copy,setv);

  { 
    char wbuf[MAXBUF+1];
    strcpy(wbuf,orig);
    if (sc[3] && !test) {
      just_testing=0;
      strcpy(wbuf,set_value(wbuf,0,3));
      just_testing=1;
    }

    if (sc[2] && !test) {
      just_testing=0;
      strcpy(wbuf,set_value(wbuf,0,2));
      just_testing=1;
    }

    if (sc[1] && !test) {
      just_testing=0;
      strcpy(wbuf,set_value(wbuf,0,1));
      just_testing=1;
    }

    if (sc[0] && !test) {
      just_testing=0;
      strcpy(wbuf,set_value(wbuf,0,0));
      just_testing=1;
    }
  }

  strcpy(setv,setval_copy);

  just_testing=0;

  *(scr++)=sc[0];
  *(scr++)=sc[1];
  *(scr++)=sc[2];
  *(scr++)=sc[3];

  return 0;
}


int   get_water_pos(void) { return water_off; }
void  set_water_pos(int x) { water_off=x; }



char* get_langdesc(void) {
#ifdef ENG_FINE
  return "fine quality technical text";
#else
  return "draft / e-mail quality technical text";
#endif
}


void module_help(void) {
  debug("This module supports SD_SYNONYMS environment variable that should\n"
        "point to an alternative 'synonyms' file, if necessary. Make sure\n"
        "to keep the copy of used alternative file for further reference.\n");
}


void md5_wrong(void) {
  debug("You have used more than one synonyms file to generate your watermarks.\n"
        "Make sure you've passed the right one using SD_SYNONYMS environment\n"
        "variable.\n");
}
