/*
 * Online help index routines for CUPS.
 *
 * Copyright © 2007-2019 by Apple Inc.
 * Copyright © 1997-2007 by Easy Software Products.
 *
 * Licensed under Apache License v2.0.  See the file "LICENSE" for more
 * information.
 */

/*
 * Include necessary headers...
 */

#include "cgi-private.h"
#include <cups/dir.h>


/*
 * List of common English words that should not be indexed...
 */

static char		help_common_words[][6] =
			{
			  "about",
			  "all",
			  "an",
			  "and",
			  "are",
			  "as",
			  "at",
			  "be",
			  "been",
			  "but",
			  "by",
			  "call",
			  "can",
			  "come",
			  "could",
			  "day",
			  "did",
			  "do",
			  "down",
			  "each",
			  "find",
			  "first",
			  "for",
			  "from",
			  "go",
			  "had",
			  "has",
			  "have",
			  "he",
			  "her",
			  "him",
			  "his",
			  "hot",
			  "how",
			  "if",
			  "in",
			  "is",
			  "it",
			  "know",
			  "like",
			  "long",
			  "look",
			  "make",
			  "many",
			  "may",
			  "more",
			  "most",
			  "my",
			  "no",
			  "now",
			  "of",
			  "on",
			  "one",
			  "or",
			  "other",
			  "out",
			  "over",
			  "said",
			  "see",
			  "she",
			  "side",
			  "so",
			  "some",
			  "sound",
			  "than",
			  "that",
			  "the",
			  "their",
			  "them",
			  "then",
			  "there",
			  "these",
			  "they",
			  "thing",
			  "this",
			  "time",
			  "to",
			  "two",
			  "up",
			  "use",
			  "was",
			  "water",
			  "way",
			  "we",
			  "were",
			  "what",
			  "when",
			  "which",
			  "who",
			  "will",
			  "with",
			  "word",
			  "would",
			  "write",
			  "you",
			  "your"
			};


/*
 * Local functions...
 */

static help_word_t	*help_add_word(help_node_t *n, const char *text);
static void		help_delete_node(help_node_t *n);
static void		help_delete_word(help_word_t *w);
static int		help_load_directory(help_index_t *hi,
			                    const char *directory,
					    const char *relative);
static int		help_load_file(help_index_t *hi,
			               const char *filename,
				       const char *relative,
				       time_t     mtime);
static help_node_t	*help_new_node(const char *filename, const char *anchor, const char *section, const char *text, time_t mtime, off_t offset, size_t length) _CUPS_NONNULL(1,3,4);
static int		help_sort_by_name(help_node_t *p1, help_node_t *p2);
static int		help_sort_by_score(help_node_t *p1, help_node_t *p2);
static int		help_sort_words(help_word_t *w1, help_word_t *w2);


/*
 * 'helpDeleteIndex()' - Delete an index, freeing all memory used.
 */

void
helpDeleteIndex(help_index_t *hi)	/* I - Help index */
{
  help_node_t	*node;			/* Current node */


  if (!hi)
    return;

  for (node = (help_node_t *)cupsArrayFirst(hi->nodes);
       node;
       node = (help_node_t *)cupsArrayNext(hi->nodes))
  {
    if (!hi->search)
      help_delete_node(node);
  }

  cupsArrayDelete(hi->nodes);
  cupsArrayDelete(hi->sorted);

  free(hi);
}


/*
 * 'helpFindNode()' - Find a node in an index.
 */

help_node_t *				/* O - Node pointer or NULL */
helpFindNode(help_index_t *hi,		/* I - Index */
             const char   *filename,	/* I - Filename */
             const char   *anchor)	/* I - Anchor */
{
  help_node_t	key;			/* Search key */


 /*
  * Range check input...
  */

  if (!hi || !filename)
    return (NULL);

 /*
  * Initialize the search key...
  */

  key.filename = (char *)filename;
  key.anchor   = (char *)anchor;

 /*
  * Return any match...
  */

  return ((help_node_t *)cupsArrayFind(hi->nodes, &key));
}


/*
 * 'helpLoadIndex()' - Load a help index from disk.
 */

help_index_t *				/* O - Index pointer or NULL */
helpLoadIndex(const char *hifile,	/* I - Index filename */
              const char *directory)	/* I - Directory that is indexed */
{
  help_index_t	*hi;			/* Help index */
  cups_file_t	*fp;			/* Current file */
  char		line[2048],		/* Line from file */
		*ptr,			/* Pointer into line */
		*filename,		/* Filename in line */
		*anchor,		/* Anchor in line */
		*sectptr,		/* Section pointer in line */
		section[1024],		/* Section name */
		*text;			/* Text in line */
  time_t	mtime;			/* Modification time */
  off_t		offset;			/* Offset into file */
  size_t	length;			/* Length in bytes */
  int		update;			/* Update? */
  help_node_t	*node;			/* Current node */
  help_word_t	*word;			/* Current word */


 /*
  * Create a new, empty index.
  */

  if ((hi = (help_index_t *)calloc(1, sizeof(help_index_t))) == NULL)
    return (NULL);

  hi->nodes  = cupsArrayNew((cups_array_func_t)help_sort_by_name, NULL);
  hi->sorted = cupsArrayNew((cups_array_func_t)help_sort_by_score, NULL);

  if (!hi->nodes || !hi->sorted)
  {
    cupsArrayDelete(hi->nodes);
    cupsArrayDelete(hi->sorted);
    free(hi);
    return (NULL);
  }

 /*
  * Try loading the existing index file...
  */

  if ((fp = cupsFileOpen(hifile, "r")) != NULL)
  {
   /*
    * Lock the file and then read the first line...
    */

    cupsFileLock(fp, 1);

    if (cupsFileGets(fp, line, sizeof(line)) && !strcmp(line, "HELPV2"))
    {
     /*
      * Got a valid header line, now read the data lines...
      */

      node = NULL;

      while (cupsFileGets(fp, line, sizeof(line)))
      {
       /*
	* Each line looks like one of the following:
	*
	*     filename mtime offset length "section" "text"
	*     filename#anchor offset length "text"
	*     SP count word
	*/

        if (line[0] == ' ')
	{
	 /*
	  * Read a word in the current node...
	  */

          if (!node || (ptr = strrchr(line, ' ')) == NULL)
	    continue;

          if ((word = help_add_word(node, ptr + 1)) != NULL)
	    word->count = atoi(line + 1);
        }
	else
	{
	 /*
	  * Add a node...
	  */

	  filename = line;

	  if ((ptr = strchr(line, ' ')) == NULL)
            break;

	  while (isspace(*ptr & 255))
            *ptr++ = '\0';

	  if ((anchor = strrchr(filename, '#')) != NULL)
	  {
            *anchor++ = '\0';
	    mtime = 0;
	  }
	  else
	    mtime = strtol(ptr, &ptr, 10);

	  offset = strtoll(ptr, &ptr, 10);
	  length = (size_t)strtoll(ptr, &ptr, 10);

	  while (isspace(*ptr & 255))
            ptr ++;

          if (!anchor)
	  {
	   /*
	    * Get section...
	    */

            if (*ptr != '\"')
	      break;

            ptr ++;
	    sectptr = ptr;

            while (*ptr && *ptr != '\"')
	      ptr ++;

            if (*ptr != '\"')
	      break;

            *ptr++ = '\0';

            strlcpy(section, sectptr, sizeof(section));

	    while (isspace(*ptr & 255))
              ptr ++;
          }
          else
            section[0] = '\0';

          if (*ptr != '\"')
	    break;

          ptr ++;
	  text = ptr;

          while (*ptr && *ptr != '\"')
	    ptr ++;

          if (*ptr != '\"')
	    break;

          *ptr++ = '\0';

	  if ((node = help_new_node(filename, anchor, section, text,
				    mtime, offset, length)) == NULL)
            break;

	  node->score = -1;

	  cupsArrayAdd(hi->nodes, node);
        }
      }
    }

    cupsFileClose(fp);
  }

 /*
  * Scan for new/updated files...
  */

  update = help_load_directory(hi, directory, NULL);

 /*
  * Remove any files that are no longer installed...
  */

  for (node = (help_node_t *)cupsArrayFirst(hi->nodes);
       node;
       node = (help_node_t *)cupsArrayNext(hi->nodes))
    if (node->score < 0)
    {
     /*
      * Delete this node...
      */

      cupsArrayRemove(hi->nodes, node);
      help_delete_node(node);
    }

 /*
  * Add nodes to the sorted array...
  */

  for (node = (help_node_t *)cupsArrayFirst(hi->nodes);
       node;
       node = (help_node_t *)cupsArrayNext(hi->nodes))
    cupsArrayAdd(hi->sorted, node);

 /*
  * Save the index if we updated it...
  */

  if (update)
    helpSaveIndex(hi, hifile);

 /*
  * Return the index...
  */

  return (hi);
}


/*
 * 'helpSaveIndex()' - Save a help index to disk.
 */

int					/* O - 0 on success, -1 on error */
helpSaveIndex(help_index_t *hi,		/* I - Index */
              const char   *hifile)	/* I - Index filename */
{
  cups_file_t	*fp;			/* Index file */
  help_node_t	*node;			/* Current node */
  help_word_t	*word;			/* Current word */


 /*
  * Try creating a new index file...
  */

  if ((fp = cupsFileOpen(hifile, "w9")) == NULL)
    return (-1);

 /*
  * Lock the file while we write it...
  */

  cupsFileLock(fp, 1);

  cupsFilePuts(fp, "HELPV2\n");

  for (node = (help_node_t *)cupsArrayFirst(hi->nodes);
       node;
       node = (help_node_t *)cupsArrayNext(hi->nodes))
  {
   /*
    * Write the current node with/without the anchor...
    */

    if (node->anchor)
    {
      if (cupsFilePrintf(fp, "%s#%s " CUPS_LLFMT " " CUPS_LLFMT " \"%s\"\n",
                         node->filename, node->anchor,
                         CUPS_LLCAST node->offset, CUPS_LLCAST node->length,
			 node->text) < 0)
        break;
    }
    else
    {
      if (cupsFilePrintf(fp, "%s %d " CUPS_LLFMT " " CUPS_LLFMT " \"%s\" \"%s\"\n",
                         node->filename, (int)node->mtime,
                         CUPS_LLCAST node->offset, CUPS_LLCAST node->length,
			 node->section ? node->section : "", node->text) < 0)
        break;
    }

   /*
    * Then write the words associated with the node...
    */

    for (word = (help_word_t *)cupsArrayFirst(node->words);
         word;
	 word = (help_word_t *)cupsArrayNext(node->words))
      if (cupsFilePrintf(fp, " %d %s\n", word->count, word->text) < 0)
        break;
  }

  cupsFileFlush(fp);

  if (cupsFileClose(fp) < 0)
    return (-1);
  else if (node)
    return (-1);
  else
    return (0);
}


/*
 * 'helpSearchIndex()' - Search an index.
 */

help_index_t *				/* O - Search index */
helpSearchIndex(help_index_t *hi,	/* I - Index */
                const char   *query,	/* I - Query string */
		const char   *section,	/* I - Limit search to this section */
		const char   *filename)	/* I - Limit search to this file */
{
  help_index_t	*search;		/* Search index */
  help_node_t	*node;			/* Current node */
  help_word_t	*word;			/* Current word */
  void		*sc;			/* Search context */
  int		matches;		/* Number of matches */


 /*
  * Range check...
  */

  if (!hi || !query)
    return (NULL);

 /*
  * Reset the scores of all nodes to 0...
  */

  for (node = (help_node_t *)cupsArrayFirst(hi->nodes);
       node;
       node = (help_node_t *)cupsArrayNext(hi->nodes))
    node->score = 0;

 /*
  * Find the first node to search in...
  */

  if (filename)
  {
    node = helpFindNode(hi, filename, NULL);
    if (!node)
      return (NULL);
  }
  else
    node = (help_node_t *)cupsArrayFirst(hi->nodes);

 /*
  * Convert the query into a regular expression...
  */

  sc = cgiCompileSearch(query);
  if (!sc)
    return (NULL);

 /*
  * Allocate a search index...
  */

  search = calloc(1, sizeof(help_index_t));
  if (!search)
  {
    cgiFreeSearch(sc);
    return (NULL);
  }

  search->nodes  = cupsArrayNew((cups_array_func_t)help_sort_by_name, NULL);
  search->sorted = cupsArrayNew((cups_array_func_t)help_sort_by_score, NULL);

  if (!search->nodes || !search->sorted)
  {
    cupsArrayDelete(search->nodes);
    cupsArrayDelete(search->sorted);
    free(search);
    cgiFreeSearch(sc);
    return (NULL);
  }

  search->search = 1;

 /*
  * Check each node in the index, adding matching nodes to the
  * search index...
  */

  for (; node; node = (help_node_t *)cupsArrayNext(hi->nodes))
    if (node->section && section && strcmp(node->section, section))
      continue;
    else if (filename && strcmp(node->filename, filename))
      continue;
    else
    {
      matches = cgiDoSearch(sc, node->text);

      for (word = (help_word_t *)cupsArrayFirst(node->words);
           word;
	   word = (help_word_t *)cupsArrayNext(node->words))
        if (cgiDoSearch(sc, word->text) > 0)
          matches += word->count;

      if (matches > 0)
      {
       /*
	* Found a match, add the node to the search index...
	*/

	node->score = matches;

	cupsArrayAdd(search->nodes, node);
	cupsArrayAdd(search->sorted, node);
      }
    }

 /*
  * Free the search context...
  */

  cgiFreeSearch(sc);

 /*
  * Return the results...
  */

  return (search);
}


/*
 * 'help_add_word()' - Add a word to a node.
 */

static help_word_t *			/* O - New word */
help_add_word(help_node_t *n,		/* I - Node */
              const char  *text)	/* I - Word text */
{
  help_word_t	*w,			/* New word */
		key;			/* Search key */


 /*
  * Create the words array as needed...
  */

  if (!n->words)
    n->words = cupsArrayNew((cups_array_func_t)help_sort_words, NULL);

 /*
  * See if the word is already added...
  */

  key.text = (char *)text;

  if ((w = (help_word_t *)cupsArrayFind(n->words, &key)) == NULL)
  {
   /*
    * Create a new word...
    */

    if ((w = calloc(1, sizeof(help_word_t))) == NULL)
      return (NULL);

    if ((w->text = strdup(text)) == NULL)
    {
      free(w);
      return (NULL);
    }

    cupsArrayAdd(n->words, w);
  }

 /*
  * Bump the counter for this word and return it...
  */

  w->count ++;

  return (w);
}


/*
 * 'help_delete_node()' - Free all memory used by a node.
 */

static void
help_delete_node(help_node_t *n)	/* I - Node */
{
  help_word_t	*w;			/* Current word */


  if (!n)
    return;

  if (n->filename)
    free(n->filename);

  if (n->anchor)
    free(n->anchor);

  if (n->section)
    free(n->section);

  if (n->text)
    free(n->text);

  for (w = (help_word_t *)cupsArrayFirst(n->words);
       w;
       w = (help_word_t *)cupsArrayNext(n->words))
    help_delete_word(w);

  cupsArrayDelete(n->words);

  free(n);
}


/*
 * 'help_delete_word()' - Free all memory used by a word.
 */

static void
help_delete_word(help_word_t *w)	/* I - Word */
{
  if (!w)
    return;

  if (w->text)
    free(w->text);

  free(w);
}


/*
 * 'help_load_directory()' - Load a directory of files into an index.
 */

static int				/* O - 0 = success, -1 = error, 1 = updated */
help_load_directory(
    help_index_t *hi,			/* I - Index */
    const char   *directory,		/* I - Directory */
    const char   *relative)		/* I - Relative path */
{
  cups_dir_t	*dir;			/* Directory file */
  cups_dentry_t	*dent;			/* Directory entry */
  char		*ext,			/* Pointer to extension */
		filename[1024],		/* Full filename */
		relname[1024];		/* Relative filename */
  int		update;			/* Updated? */
  help_node_t	*node;			/* Current node */


 /*
  * Open the directory and scan it...
  */

  if ((dir = cupsDirOpen(directory)) == NULL)
    return (0);

  update = 0;

  while ((dent = cupsDirRead(dir)) != NULL)
  {
   /*
    * Skip "." files...
    */

    if (dent->filename[0] == '.')
      continue;

   /*
    * Get absolute and relative filenames...
    */

    snprintf(filename, sizeof(filename), "%s/%s", directory, dent->filename);
    if (relative)
      snprintf(relname, sizeof(relname), "%s/%s", relative, dent->filename);
    else
      strlcpy(relname, dent->filename, sizeof(relname));

   /*
    * Check if we have a HTML file...
    */

    if ((ext = strstr(dent->filename, ".html")) != NULL &&
        (!ext[5] || !strcmp(ext + 5, ".gz")))
    {
     /*
      * HTML file, see if we have already indexed the file...
      */

      if ((node = helpFindNode(hi, relname, NULL)) != NULL)
      {
       /*
        * File already indexed - check dates to confirm that the
	* index is up-to-date...
	*/

        if (node->mtime == dent->fileinfo.st_mtime)
	{
	 /*
	  * Same modification time, so mark all of the nodes
	  * for this file as up-to-date...
	  */

          for (; node; node = (help_node_t *)cupsArrayNext(hi->nodes))
	    if (!strcmp(node->filename, relname))
	      node->score = 0;
	    else
	      break;

          continue;
	}
      }

      update = 1;

      help_load_file(hi, filename, relname, dent->fileinfo.st_mtime);
    }
    else if (S_ISDIR(dent->fileinfo.st_mode))
    {
     /*
      * Process sub-directory...
      */

      if (help_load_directory(hi, filename, relname) == 1)
        update = 1;
    }
  }

  cupsDirClose(dir);

  return (update);
}


/*
 * 'help_load_file()' - Load a HTML files into an index.
 */

static int				/* O - 0 = success, -1 = error */
help_load_file(
    help_index_t *hi,			/* I - Index */
    const char   *filename,		/* I - Filename */
    const char   *relative,		/* I - Relative path */
    time_t       mtime)			/* I - Modification time */
{
  cups_file_t	*fp;			/* HTML file */
  help_node_t	*node;			/* Current node */
  char		line[1024],		/* Line from file */
		temp[1024],		/* Temporary word */
                section[1024],		/* Section */
		*ptr,			/* Pointer into line */
		*anchor,		/* Anchor name */
		*text;			/* Text for anchor */
  off_t		offset;			/* File offset */
  char		quote;			/* Quote character */
  help_word_t	*word;			/* Current word */
  int		wordlen;		/* Length of word */


  if ((fp = cupsFileOpen(filename, "r")) == NULL)
    return (-1);

  node   = NULL;
  offset = 0;

  strlcpy(section, "Other", sizeof(section));

  while (cupsFileGets(fp, line, sizeof(line)))
  {
   /*
    * Look for "<TITLE>", "<A NAME", or "<!-- SECTION:" prefix...
    */

    if ((ptr = strstr(line, "<!-- SECTION:")) != NULL)
    {
     /*
      * Got section line, copy it!
      */

      for (ptr += 13; isspace(*ptr & 255); ptr ++);

      strlcpy(section, ptr, sizeof(section));
      if ((ptr = strstr(section, "-->")) != NULL)
      {
       /*
        * Strip comment stuff from end of line...
	*/

        for (*ptr-- = '\0'; ptr > line && isspace(*ptr & 255); *ptr-- = '\0');

	if (isspace(*ptr & 255))
	  *ptr = '\0';
      }
      continue;
    }

    for (ptr = line; (ptr = strchr(ptr, '<')) != NULL;)
    {
      ptr ++;

      if (!_cups_strncasecmp(ptr, "TITLE>", 6))
      {
       /*
        * Found the title...
	*/

	anchor = NULL;
	ptr += 6;
      }
      else
      {
        char *idptr;			/* Pointer to ID */

	if (!_cups_strncasecmp(ptr, "A NAME=", 7))
	  ptr += 7;
	else if ((idptr = strstr(ptr, " ID=")) != NULL)
	  ptr = idptr + 4;
	else if ((idptr = strstr(ptr, " id=")) != NULL)
	  ptr = idptr + 4;
	else
	  continue;

       /*
        * Found an anchor...
	*/

	if (*ptr == '\"' || *ptr == '\'')
	{
	 /*
	  * Get quoted anchor...
	  */

	  quote  = *ptr;
          anchor = ptr + 1;
	  if ((ptr = strchr(anchor, quote)) != NULL)
	    *ptr++ = '\0';
	  else
	    break;
	}
	else
	{
	 /*
	  * Get unquoted anchor...
	  */

          anchor = ptr + 1;

	  for (ptr = anchor; *ptr && *ptr != '>' && !isspace(*ptr & 255); ptr ++);

	  if (*ptr != '>')
	    *ptr++ = '\0';
	  else
	    break;
	}

       /*
        * Got the anchor, now lets find the end...
	*/

        while (*ptr && *ptr != '>')
	  ptr ++;

        if (*ptr != '>')
	  break;

        *ptr++ = '\0';
      }

     /*
      * Now collect text for the link...
      */

      text = ptr;
      while ((ptr = strchr(text, '<')) == NULL)
      {
	ptr = text + strlen(text);
	if (ptr >= (line + sizeof(line) - 2))
	  break;

        *ptr++ = ' ';

        if (!cupsFileGets(fp, ptr, sizeof(line) - (size_t)(ptr - line) - 1))
	  break;
      }

      *ptr = '\0';

      if (node)
	node->length = (size_t)(offset - node->offset);

      if (!*text)
      {
        node = NULL;
        break;
      }

      if ((node = helpFindNode(hi, relative, anchor)) != NULL)
      {
       /*
	* Node already in the index, so replace the text and other
	* data...
	*/

        cupsArrayRemove(hi->nodes, node);

        if (node->section)
	  free(node->section);

	if (node->text)
	  free(node->text);

        if (node->words)
	{
	  for (word = (help_word_t *)cupsArrayFirst(node->words);
	       word;
	       word = (help_word_t *)cupsArrayNext(node->words))
	    help_delete_word(word);

	  cupsArrayDelete(node->words);
	  node->words = NULL;
	}

	node->section = section[0] ? strdup(section) : NULL;
	node->text    = strdup(text);
	node->mtime   = mtime;
	node->offset  = offset;
	node->score   = 0;
      }
      else
      {
       /*
	* New node...
	*/

        node = help_new_node(relative, anchor, section, text, mtime, offset, 0);
      }

     /*
      * Go through the text value and replace tabs and newlines with
      * whitespace and eliminate extra whitespace...
      */

      for (ptr = node->text, text = node->text; *ptr;)
	if (isspace(*ptr & 255))
	{
	  while (isspace(*ptr & 255))
	    ptr ++;

	  *text++ = ' ';
        }
	else if (text != ptr)
	  *text++ = *ptr++;
	else
	{
	  text ++;
	  ptr ++;
	}

      *text = '\0';

     /*
      * (Re)add the node to the array...
      */

      cupsArrayAdd(hi->nodes, node);

      if (!anchor)
        node = NULL;
      break;
    }

    if (node)
    {
     /*
      * Scan this line for words...
      */

      for (ptr = line; *ptr; ptr ++)
      {
       /*
	* Skip HTML stuff...
	*/

	if (*ptr == '<')
	{
          if (!strncmp(ptr, "<!--", 4))
	  {
	   /*
	    * Skip HTML comment...
	    */

            if ((text = strstr(ptr + 4, "-->")) == NULL)
	      ptr += strlen(ptr) - 1;
	    else
	      ptr = text + 2;
	  }
	  else
	  {
	   /*
            * Skip HTML element...
	    */

            for (ptr ++; *ptr && *ptr != '>'; ptr ++)
	    {
	      if (*ptr == '\"' || *ptr == '\'')
	      {
		for (quote = *ptr++; *ptr && *ptr != quote; ptr ++);

		if (!*ptr)
		  ptr --;
	      }
	    }

	    if (!*ptr)
	      ptr --;
          }

          continue;
	}
	else if (*ptr == '&')
	{
	 /*
	  * Skip HTML entity...
	  */

	  for (ptr ++; *ptr && *ptr != ';'; ptr ++);

	  if (!*ptr)
	    ptr --;

	  continue;
	}
	else if (!isalnum(*ptr & 255))
          continue;

       /*
	* Found the start of a word, search until we find the end...
	*/

	for (text = ptr, ptr ++; *ptr && isalnum(*ptr & 255); ptr ++);

	wordlen = (int)(ptr - text);

        memcpy(temp, text, (size_t)wordlen);
	temp[wordlen] = '\0';

        ptr --;

	if (wordlen > 1 && !bsearch(temp, help_common_words,
	                            (sizeof(help_common_words) /
				     sizeof(help_common_words[0])),
				    sizeof(help_common_words[0]),
				    (int (*)(const void *, const void *))
				        _cups_strcasecmp))
          help_add_word(node, temp);
      }
    }

   /*
    * Get the offset of the next line...
    */

    offset = cupsFileTell(fp);
  }

  cupsFileClose(fp);

  if (node)
    node->length = (size_t)(offset - node->offset);

  return (0);
}


/*
 * 'help_new_node()' - Create a new node and add it to an index.
 */

static help_node_t *			/* O - Node pointer or NULL on error */
help_new_node(const char   *filename,	/* I - Filename */
              const char   *anchor,	/* I - Anchor */
	      const char   *section,	/* I - Section */
	      const char   *text,	/* I - Text */
	      time_t       mtime,	/* I - Modification time */
              off_t        offset,	/* I - Offset in file */
	      size_t       length)	/* I - Length in bytes */
{
  help_node_t	*n;			/* Node */


  n = (help_node_t *)calloc(1, sizeof(help_node_t));
  if (!n)
    return (NULL);

  n->filename = strdup(filename);
  n->anchor   = anchor ? strdup(anchor) : NULL;
  n->section  = (section && *section) ? strdup(section) : NULL;
  n->text     = strdup(text);
  n->mtime    = mtime;
  n->offset   = offset;
  n->length   = length;

  return (n);
}


/*
 * 'help_sort_nodes_by_name()' - Sort nodes by section, filename, and anchor.
 */

static int				/* O - Difference */
help_sort_by_name(help_node_t *n1,	/* I - First node */
                  help_node_t *n2)	/* I - Second node */
{
  int		diff;			/* Difference */


  if ((diff = strcmp(n1->filename, n2->filename)) != 0)
    return (diff);

  if (!n1->anchor && !n2->anchor)
    return (0);
  else if (!n1->anchor)
    return (-1);
  else if (!n2->anchor)
    return (1);
  else
    return (strcmp(n1->anchor, n2->anchor));
}


/*
 * 'help_sort_nodes_by_score()' - Sort nodes by score and text.
 */

static int				/* O - Difference */
help_sort_by_score(help_node_t *n1,	/* I - First node */
                   help_node_t *n2)	/* I - Second node */
{
  int		diff;			/* Difference */


  if (n1->score != n2->score)
    return (n2->score - n1->score);

  if (n1->section && !n2->section)
    return (1);
  else if (!n1->section && n2->section)
    return (-1);
  else if (n1->section && n2->section &&
           (diff = strcmp(n1->section, n2->section)) != 0)
    return (diff);

  return (_cups_strcasecmp(n1->text, n2->text));
}


/*
 * 'help_sort_words()' - Sort words alphabetically.
 */

static int				/* O - Difference */
help_sort_words(help_word_t *w1,	/* I - Second word */
                help_word_t *w2)	/* I - Second word */
{
  return (_cups_strcasecmp(w1->text, w2->text));
}
