/*  $Id$

    Part of SWI-Prolog

    Author:        Jan Wielemaker and Richard O'Keefe
    E-mail:        wielemak@science.uva.nl
    WWW:           http://www.swi-prolog.org
    Copyright (C): 1985-2006, University of Amsterdam

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#define _ISOC99_SOURCE 1		/* fwprintf(), etc prototypes */
#include "util.h"
#include "catalog.h"
#include <stdio.h>
#include <wctype.h>
#include <string.h>
#include <stdlib.h>
#define DTD_MINOR_ERRORS 1
#include <dtd.h>			/* error codes */

#ifdef __WINDOWS__
#define swprintf _snwprintf
#endif

#ifdef _REENTRANT
#include <pthread.h>

static pthread_mutex_t catalog_mutex = PTHREAD_MUTEX_INITIALIZER;
#define LOCK() pthread_mutex_lock(&catalog_mutex)
#define UNLOCK() pthread_mutex_unlock(&catalog_mutex)
#else
#define LOCK()
#define UNLOCK()
#endif

#ifndef MAXPATHLEN
#define MAXPATHLEN 1024
#endif
#ifndef MAXLINE
#define MAXLINE 1024
#endif
#ifndef EOS
#define EOS '\0'
#endif
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif

#define streq(s1, s2) istreq(s1, s2)
#define uc(p) (*(p))

typedef struct catalogue_item *catalogue_item_ptr;
struct catalogue_item
{ catalogue_item_ptr next;
  int kind;
  ichar const *target;
  ichar const *replacement;
};

static catalogue_item_ptr first_item = 0, last_item = 0;

typedef struct _catalog_file
{ ichar *file;
  struct _catalog_file *next;
  int loaded;				/* did we parse this file? */
  catalogue_item_ptr first_item;	/* List of items in the file */
  catalogue_item_ptr last_item;
} catalog_file;

static catalog_file *catalog;

#ifdef __WINDOWS__
#define isDirSep(c) ((c) == '/' || (c) == '\\')
#define DIRSEPSTR L"\\"
#else
#define isDirSep(c) ((c) == '/')
#define DIRSEPSTR L"/"
#endif

static ichar *
DirName(const ichar *f, ichar *dir)
{ const ichar *base, *p;

  for (base = p = f; *p; p++)
  { if (isDirSep(*p) && p[1] != EOS)
      base = p;
  }
  if (base == f)
  { if (isDirSep(*f))
      istrcpy(dir, DIRSEPSTR);
    else
      istrcpy(dir, L".");
  } else
  { istrncpy(dir, f, base - f);
    dir[base - f] = EOS;
  }

  return dir;
}


int
is_absolute_path(const ichar *name)
{ if (isDirSep(name[0])
#ifdef __WINDOWS__
      || (iswalpha(uc(name)) && name[1] == ':')
#endif
    )
    return TRUE;

  return FALSE;
}

int
is_url(const ichar *name)
{ if ( iswalpha(name[0]) )
  { while(*name && iswalpha(*name))
      name++;
    if ( *name && name[0] == ':' && name[1] == '/' && name[2] == '/' )
      return TRUE;
  }

  return FALSE;
}


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
localpath() creates an absolute  path  for   name  relative  to ref. The
returned path must be freed using sgml_free() when done.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

ichar *
localpath(const ichar *ref, const ichar *name)
{ ichar *local;

  if (!ref || is_absolute_path(name))
    local = istrdup(name);
  else
  { ichar buf[MAXPATHLEN];

    DirName(ref, buf);
    istrcat(buf, DIRSEPSTR);
    istrcat(buf, name);

    local = istrdup(buf);
  }

  if (!local)
    sgml_nomem();

  return local;
}


int
register_catalog_file_unlocked(const ichar *file, catalog_location where)
{ catalog_file **f = &catalog;
  catalog_file *cf;

  for (; *f; f = &(*f)->next)
  { cf = *f;

    if (istreq(cf->file, file))
      return TRUE;		/* existing, move? */
  }

  cf = sgml_malloc(sizeof(*cf));
  memset(cf, 0, sizeof(*cf));
  cf->file = istrdup(file);
  if (!cf->file)
    sgml_nomem();

  if (where == CTL_END)
  { cf->next = NULL;
    *f = cf;
  } else
  { cf->next = catalog;
    catalog = cf;
  }

  return TRUE;
}


static wchar_t *
wgetenv(const char *name)
{ const char *vs;

  if ( (vs = getenv(name)) )
  { size_t wl = mbstowcs(NULL, vs, 0);

    if ( wl > 0 )
    { wchar_t *ws = sgml_malloc((wl+1)*sizeof(wchar_t));
      mbstowcs(ws, vs, wl+1);

      return ws;
    }
  }

  return NULL;
}


static void
init_catalog()
{ static int done = FALSE;

  LOCK();
  if ( !done++ )
  { ichar *path = wgetenv("SGML_CATALOG_FILES");

    if (!path)
    { UNLOCK();
      return;
    }

    while (*path)
    { ichar buf[MAXPATHLEN];
      ichar *s;

      if ((s = istrchr(path, L':')))
      { istrncpy(buf, path, s - path);
	buf[s - path] = '\0';
	path = s + 1;
	if ( buf[0] )			/* skip empty entries */
	  register_catalog_file_unlocked(buf, CTL_START);
      } else
      { if ( path[0] )			/* skip empty entries */
	  register_catalog_file_unlocked(path, CTL_START);
	break;
      }
    }
  }
  UNLOCK();
}


int
register_catalog_file(const ichar *file, catalog_location where)
{ int rc;

  init_catalog();

  LOCK();
  rc = register_catalog_file_unlocked(file, where);
  UNLOCK();

  return rc;
}


		 /*******************************
		 *     CATALOG FILE PARSING	*
		 *******************************/

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The code from here to the  end  of   this  file  was  written by Richard
O'Keefe and modified by Jan Wielemaker to fit   in  with the rest of the
parser.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

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

/*  OVERRIDE YES/NO
    sets a boolean flag initialised to NO.
    The value of this flag is stored as part of each entry.
    (PUBLIC|DOCTYPE|ENTITY)&YES will match whether a system identifier
    was provided in the source document or not;
    (PUBLIC|DOCTYPE|ENTITY)&NO will only match if a system identifier
    was not provided.
*/

/*  catalogue =
    (   PUBLIC  pubid filename
    |   SYSTEM  sysid filename
    |   DOCTYPE name  filename
    |   ENTITY  name  filename
    |   OVERRIDE YES
    |   OVERRIDE NO
    |   BASE          filename
    |   junk
    )*
*/


/*  Keywords are matched ignoring case.  */

static int
ci_streql(ichar const *a, ichar const *b)
{ return istrcaseeq(a, b);
}

/*  Names may be matched heading case in XML.  */

static int
cs_streql(ichar const *a, ichar const *b)
{ return istreq(a, b);
}

/*  Any other word or any quoted string is reported as CAT_OTHER.
    When we are not looking for the beginning of an entry, the only
    positive outcome is CAT_OTHER.
*/

static int
scan_overflow(size_t buflen)
{ gripe(NULL, ERC_REPRESENTATION, L"token length");

  return EOF;
}

static int
scan(FILE* src, ichar *buffer, size_t buflen, int kw_expected)
{ int c, q;
  ichar *p = buffer, *e = p + buflen - 1;

  for (;;)
  { c = getc(src);
    if (c <= ' ')
    { if (c < 0)
	return EOF;
      continue;
    }
    if (c == '-')
    { c = getc(src);
      if (c != '-')
      { *p++ = '-';
	break;
      }
      for (;;)
      { c = getc(src);
	if (c < 0)
	  return EOF;
	if (c == '-')
	{ c = getc(src);
	  if (c < 0)
	    return EOF;
	  if (c == '-')
	    break;
	}
      }
      continue;
    }
    if (c == '"' || c == '\'')
    { q = c;
      for (;;)
      { c = getc(src);
	if (c < 0)
	  return EOF;
	if (c == q)
	{ *p = '\0';
	  return CAT_OTHER;
	}
	if (p == e)
	  return scan_overflow(buflen);
	*p++ = c;
      }
    }
    break;
  }
  /*  We reach here if there is an unquoted token.   */
  /*  Don't try "PUBLIC--well/sortof--'foo' 'bar'"   */
  /*  because hyphens are allowed in unquoted words  */
  /*  and so are slashes and a bunch of other stuff. */
  /*  To keep this code simple, an unquoted token    */
  /*  ends at EOF, ', ", or layout.                  */
  while (c > ' ' && c != '"' && c != '\'')
  { if (p == e)
      return scan_overflow(buflen);
    *p++ = c;
    c = getc(src);
  }
  *p = '\0';
  if (kw_expected)
  { if (ci_streql(buffer, L"public"))
      return CAT_PUBLIC;
    if (ci_streql(buffer, L"system"))
      return CAT_SYSTEM;
    if (ci_streql(buffer, L"entity"))
      return CAT_ENTITY;
    if (ci_streql(buffer, L"doctype"))
      return CAT_DOCTYPE;
    if (ci_streql(buffer, L"override"))
      return CAT_OVERRIDE;
    if (ci_streql(buffer, L"base"))
      return CAT_BASE;
  }
  return CAT_OTHER;
}

/*  The strings can represent names (taken verbatim),
    system identifiers (ditto), or public identifiers (squished).
    We need to squish, and we need to copy.  When it comes to
    squishing, we don't need to worry about Unicode spaces,
    because public identifiers aren't allow to have any characters
    that aren't in ASCII.
*/

static void
squish(ichar *pubid)
{ ichar const *s = (ichar const *) pubid;
  ichar *d = (ichar *) pubid;
  ichar c;
  int w;

  w = 1;
  while ((c = *s++) != '\0')
  { if (c <= ' ')
    { if (!w)
	*d++ = ' ', w = 1;
    } else
    { *d++ = c, w = 0;
    }
  }
  if (w && d != (ichar *) pubid)
    d--;
  *d = '\0';
}

/*  We represent a catalogue internally by a list of
    (CAT_xxx, string, string)
    triples.
*/

static void
load_one_catalogue(catalog_file * file)
{ FILE *src = wfopen(file->file, "r");
  ichar buffer[2 * FILENAME_MAX];
  ichar base[2 * FILENAME_MAX];
  ichar *p;
  int t;
  catalogue_item_ptr this_item;
  int override = 0;

  if ( !src )
  { gripe(NULL, ERC_NO_CATALOGUE, file->file);
    return;
  }

  (void) istrcpy(base, file->file);
  p = base + istrlen(base);
  while (p != base && !isDirSep(p[-1]))
    p--;

  for (;;)
  { t = scan(src, buffer, sizeof(buffer), 1);
    switch (t)
    { case CAT_BASE:
	if (scan(src, buffer, sizeof(buffer), 0) == EOF)
	  break;
	(void) istrcpy(base, buffer);
	p = base + istrlen(base);
	if (p != base && !isDirSep(p[-1]))
	  *p++ = '/';
	continue;
      case CAT_OVERRIDE:
	if (scan(src, buffer, sizeof(buffer), 0) == EOF)
	  break;
	override = towlower(buffer[0]) == 'y' ? CAT_OVERRIDE : 0;
	continue;
      case CAT_PUBLIC:
      case CAT_SYSTEM:
      case CAT_ENTITY:
      case CAT_DOCTYPE:
	this_item = sgml_malloc(sizeof *this_item);
	if (scan(src, buffer, sizeof buffer, 0) == EOF)
	  break;
	if (t == CAT_PUBLIC)
	  squish(buffer);
	this_item->next = 0;
	this_item->kind = t == CAT_SYSTEM ? t : t + override;
	this_item->target = istrdup(buffer);

	if (scan(src, buffer, sizeof buffer, 0) == EOF)
	  break;

	if (is_absolute_path(buffer) || p == base)
	{ this_item->replacement = istrdup(buffer);
	} else
        { (void) istrcpy(p, buffer);
          this_item->replacement = istrdup(base);
        }

	if (file->first_item == 0)
	{ file->first_item = this_item;
	} else
	{ file->last_item->next = this_item;
	}

	file->last_item = this_item;
	continue;
      case EOF:
	break;
      default:
	continue;
    }
    break;
  }

  fclose(src);
}


/*  To look up a DTD:
    f = find_in_catalogue(CAT_DOCTYPE, name, pubid, sysid, ci);
    If it cannot otherwise be found and name is not null,
    ${name}.dtd will be returned.

    To look up a parameter entity:
    f = find_in_catalogue(CAT_PENTITY, name, pubid, sysid, ci);
    The name may begin with a % but need not; if it doesn't
    a % will be prefixed for the search.
    If it cannot otherwise be found ${name}.pen will be returned.

    To look up an ordinary entity:
    f = find_in_catalogue(CAT_ENTITY, name, pubid, sysid, ci);
    If the name begins with a % this is just like a CAT_PENTITY search.
    If it cannot otherwise be found %{name}.ent will be returned.

    The full catalogue format allows for NOTATION (which we still need
    for XML), SGMLDECL, DTDDECL, and LINKTYPE.  At the moment, only
    notation is plausible.  To handle such things,
    f = find_in_catalogue(CAT_OTHER, name, pubid, sysid, ci);
    If it cannot be found, NULL is returned.

    The name, pubid, and sysid may each be NULL.   It doesn't really
    make sense for them all to be NULL.

    For SGML, name matching (DOCTYPE, ENTITY) should normally ignore
    alphabetic case.  Pass ci=1 to make this happen.  For XML, name
    matching must heed alphabetic case.  Pass ci=0 to make that happen.

    A CAT_DOCTYPE, CAT_ENTITY, or CAT_PENTITY search doesn't really make
    sense withint a name, so if the name should happen to be 0, the search
    kind is converted to CAT_OTHER.
*/

ichar const *
find_in_catalogue(int kind,
		  ichar const *name,
		  ichar const *pubid, ichar const *sysid, int ci)
{ ichar penname[FILENAME_MAX];
  const size_t penlen = sizeof(penname)/sizeof(ichar);
  catalogue_item_ptr item;
  ichar const *result;
  catalog_file *catfile;

  init_catalog();

  if ( name == 0 )
  { kind = CAT_OTHER;
  } else
  { switch (kind)
    { case CAT_OTHER:
      case CAT_DOCTYPE:
	break;
      case CAT_PENTITY:
	if (name[0] != '%')
	{ penname[0] = '%';
	  (void) istrcpy(penname + 1, name);
	  name = penname;
	}
	break;
      case CAT_ENTITY:
	if (name[0] == '%')
	{ kind = CAT_PENTITY;
	}
	break;
      default:
	return 0;
    }
  }

  result = 0;
  for (catfile = catalog;; catfile = catfile->next)
  { if (catfile)
    { if (!catfile->loaded)
      { load_one_catalogue(catfile);
	catfile->loaded = TRUE;
      }
      item = catfile->first_item;
    } else
      item = first_item;

    for (; item != 0; item = item->next)
    { switch (item->kind)
      { case CAT_PUBLIC:
	  if (sysid != 0)
	    break;
	/*FALLTHROUGH*/
	case OVR_PUBLIC:
	  if (pubid != 0 && result == 0 && cs_streql(pubid, item->target))
	    result = item->replacement;
	  break;
	case CAT_SYSTEM:
	  if (sysid != 0 && cs_streql(sysid, item->target))
	    return item->replacement;
	  break;
	case CAT_DOCTYPE:
	  if (sysid != 0)
	    break;
	/*FALLTHROUGH*/
	case OVR_DOCTYPE:
	  if (name != 0 && kind == CAT_DOCTYPE && result == 0
	      && (ci ? ci_streql : cs_streql) (name, item->target))
	    result = item->replacement;
	  break;
	case CAT_ENTITY:
	  if (sysid != 0)
	    break;
	 /*FALLTHROUGH*/ case OVR_ENTITY:
	  if (name != 0 && kind >= CAT_ENTITY && result == 0
	      && (ci ? ci_streql : cs_streql) (name, item->target))
	    result = item->replacement;
	  break;
	default:
	  break;
      }
    }

    if (!catfile)
      break;
  }
  if ( result != 0 )
    return result;
  if ( sysid != 0 )
    return sysid;
  if ( kind == CAT_OTHER || kind == CAT_DOCTYPE )
    return 0;

  if ( istrlen(name)+4+1 > penlen )
  { gripe(NULL, ERC_REPRESENTATION, L"entity name");
    return NULL;
  }

  item = sgml_malloc(sizeof(*item));
  item->next = 0;
  item->kind = kind;
  item->target = istrdup(name);

  switch (kind)
  { case CAT_DOCTYPE:
      (void) swprintf(penname, penlen, L"%ls.dtd", name);
      break;
    case CAT_PENTITY:
      item->kind = CAT_ENTITY;
      (void) swprintf(penname, penlen, L"%ls.pen", name + 1);
      break;
    case CAT_ENTITY:
      (void) swprintf(penname, penlen, L"%ls.ent", name);
      break;
    default:
      abort();
  }

  item->replacement = istrdup(penname);
  if (first_item == 0)
  { first_item = item;
  } else
  { last_item->next = item;
  }
  last_item = item;

  return item->replacement;
}

