root/1.8.3/tags/p6/src/help.c

Revision 1167, 15.3 KB (checked in by shawnw, 13 months ago)

Merge devel into trunk for p6 release

Line 
1/**
2 * \file help.c
3 *
4 * \brief The PennMUSH help system.
5 *
6 *
7 */
8#include "config.h"
9#include <stdlib.h>
10#include <string.h>
11#include <ctype.h>
12#include <stdio.h>
13#include "conf.h"
14#include "externs.h"
15#include "command.h"
16#include "htab.h"
17#include "help.h"
18#include "log.h"
19#include "ansi.h"
20#include "parse.h"
21#include "pueblo.h"
22#include "flags.h"
23#include "dbdefs.h"
24#include "mymalloc.h"
25#include "confmagic.h"
26
27HASHTAB help_files;  /**< Help filenames hash table */
28
29static int help_init = 0;
30
31static void do_new_spitfile(dbref player, char *arg1, help_file *help_dat);
32static const char *string_spitfile(help_file *help_dat, char *arg1);
33static help_indx *help_find_entry(help_file *help_dat, const char *the_topic);
34static char **list_matching_entries(const char *pattern,
35                                    help_file *help_dat, int *len);
36static void free_entry_list(char **);
37static const char *normalize_entry(help_file *help_dat, const char *arg1);
38
39static void help_build_index(help_file *h, int restricted);
40
41/** Linked list of help topic names. */
42typedef struct TLIST {
43  char topic[TOPIC_NAME_LEN + 1];       /**< Name of topic */
44  struct TLIST *next;                   /**< Pointer to next list entry */
45} tlist;
46
47tlist *top = NULL;   /**< Pointer to top of linked list of topic names */
48
49help_indx *topics = NULL;  /**< Pointer to linked list of topic indexes */
50unsigned num_topics = 0;   /**< Number of topics loaded */
51unsigned top_topics = 0;   /**< Maximum number of topics loaded */
52
53static void write_topic(long int p);
54
55COMMAND(cmd_helpcmd)
56{
57  help_file *h;
58
59  h = hashfind(cmd->name, &help_files);
60
61  if (!h) {
62    notify(player, T("That command is unavailable."));
63    return;
64  }
65
66  if (h->admin && !Hasprivs(player)) {
67    notify(player, T("You don't look like an admin to me."));
68    return;
69  }
70
71  if (wildcard(arg_left)) {
72    int len = 0;
73    char **entries;
74
75    entries = list_matching_entries(arg_left, h, &len);
76    if (len == 0)
77      notify_format(player, T("No entries matching '%s' were found."),
78                    arg_left);
79    else if (len == 1)
80      do_new_spitfile(player, *entries, h);
81    else {
82      char buff[BUFFER_LEN];
83      char *bp;
84
85      bp = buff;
86      arr2list(entries, len, buff, &bp, ", ");
87      *bp = '\0';
88      notify_format(player, T("Here are the entries which match '%s':\n%s"),
89                    arg_left, buff);
90    }
91    free_entry_list(entries);
92  } else
93    do_new_spitfile(player, arg_left, h);
94}
95
96/** Initialize the helpfile hashtable, which contains the names of thes
97 * help files.
98 */
99void
100init_help_files(void)
101{
102  hashinit(&help_files, 8);
103  help_init = 1;
104}
105
106/** Add new help command. This function is
107 * the basis for the help_command directive in mush.cnf. It creates
108 * a new help entry for the hash table, builds a help index,
109 * and adds the new command to the command table.
110 * \param command_name name of help command to add.
111 * \param filename name of the help file to use for this command.
112 * \param admin if 1, this command reads admin topics, rather than standard.
113 */
114void
115add_help_file(const char *command_name, const char *filename, int admin)
116{
117  help_file *h;
118
119  if (help_init == 0)
120    init_help_files();
121
122  if (!command_name || !filename || !*command_name || !*filename)
123    return;
124
125  /* If there's already an entry for it, complain */
126  h = hashfind(strupper(command_name), &help_files);
127  if (h) {
128    do_rawlog(LT_ERR, T("Duplicate help_command %s ignored."), command_name);
129    return;
130  }
131
132  h = mush_malloc(sizeof *h, "help_file.entry");
133  h->command = mush_strdup(strupper(command_name), "help_file.command");
134  h->file = mush_strdup(filename, "help_file.filename");
135  h->entries = 0;
136  h->indx = NULL;
137  h->admin = admin;
138  help_build_index(h, h->admin);
139  if (!h->indx) {
140    mush_free(h->command, "help_file.command");
141    mush_free(h->file, "help_file.filename");
142    mush_free(h, "help_file.entry");
143    return;
144  }
145  (void) command_add(h->command, CMD_T_ANY | CMD_T_NOPARSE, NULL, 0, NULL,
146                     cmd_helpcmd);
147  hashadd(h->command, h, &help_files);
148}
149
150/** Rebuild a help file index.
151 * \verbatim
152 * This command implements @readcache.
153 * \endverbatim
154 * \param player the enactor.
155 */
156void
157help_reindex(dbref player)
158{
159  help_file *curr;
160
161  for (curr = (help_file *) hash_firstentry(&help_files);
162       curr; curr = (help_file *) hash_nextentry(&help_files)) {
163    if (curr->indx) {
164      mush_free((Malloc_t) curr->indx, "help_index");
165      curr->entries = 0;
166    }
167    help_build_index(curr, curr->admin);
168  }
169  if (player != NOTHING) {
170    notify(player, T("Help files reindexed."));
171    do_rawlog(LT_WIZ, T("Help files reindexed by %s(#%d)"), Name(player),
172              player);
173  } else
174    do_rawlog(LT_WIZ, T("Help files reindexed."));
175}
176
177static void
178do_new_spitfile(dbref player, char *arg1, help_file *help_dat)
179{
180  help_indx *entry = NULL;
181  FILE *fp;
182  char *p, line[LINE_SIZE + 1];
183  char the_topic[LINE_SIZE + 2];
184  int default_topic = 0;
185  size_t n;
186
187  if (*arg1 == '\0') {
188    default_topic = 1;
189    arg1 = (char *) help_dat->command;
190  } else if (*arg1 == '&') {
191    notify(player, T("Help topics don't start with '&'."));
192    return;
193  }
194  if (strlen(arg1) > LINE_SIZE)
195    *(arg1 + LINE_SIZE) = '\0';
196
197  if (help_dat->admin) {
198    sprintf(the_topic, "&%s", arg1);
199  } else
200    strcpy(the_topic, arg1);
201
202  if (!help_dat->indx || help_dat->entries == 0) {
203    notify(player, T("Sorry, that command is temporarily unvailable."));
204    do_rawlog(LT_ERR, T("No index for %s."), help_dat->command);
205    return;
206  }
207
208  entry = help_find_entry(help_dat, the_topic);
209  if (!entry && default_topic)
210    entry = help_find_entry(help_dat, (help_dat->admin ? "&help" : "help"));
211
212  if (!entry) {
213    notify_format(player, T("No entry for '%s'."), arg1);
214    return;
215  }
216
217  if ((fp = fopen(help_dat->file, FOPEN_READ)) == NULL) {
218    notify(player, T("Sorry, that function is temporarily unavailable."));
219    do_log(LT_ERR, 0, 0, T("Can't open text file %s for reading"),
220           help_dat->file);
221    return;
222  }
223  if (fseek(fp, entry->pos, 0) < 0L) {
224    notify(player, T("Sorry, that function is temporarily unavailable."));
225    do_rawlog(LT_ERR, T("Seek error in file %s"), help_dat->file);
226    return;
227  }
228  strcpy(the_topic, strupper(entry->topic + (*entry->topic == '&')));
229  /* ANSI topics */
230  if (ShowAnsi(player)) {
231    char ansi_topic[LINE_SIZE + 10];
232    sprintf(ansi_topic, "%s%s%s", ANSI_HILITE, the_topic, ANSI_END);
233    notify(player, ansi_topic);
234  } else
235    notify(player, the_topic);
236
237  if (SUPPORT_PUEBLO)
238    notify_noenter(player, open_tag("SAMP"));
239  for (n = 0; n < BUFFER_LEN; n++) {
240    if (fgets(line, LINE_SIZE, fp) == NULL)
241      break;
242    if (line[0] == '&')
243      break;
244    if (line[0] == '\n') {
245      notify(player, " ");
246    } else {
247      for (p = line; *p != '\0'; p++)
248        if (*p == '\n')
249          *p = '\0';
250      notify(player, line);
251    }
252  }
253  if (SUPPORT_PUEBLO)
254    notify(player, close_tag("SAMP"));
255  fclose(fp);
256  if (n >= BUFFER_LEN)
257    notify_format(player, T("%s output truncated."), help_dat->command);
258}
259
260
261static help_indx *
262help_find_entry(help_file *help_dat, const char *the_topic)
263{
264  help_indx *entry = NULL;
265
266  if (help_dat->entries < 10) { /* Just do a linear search for small files */
267    size_t n;
268    for (n = 0; n < help_dat->entries; n++) {
269      if (string_prefix(help_dat->indx[n].topic, the_topic)) {
270        entry = &help_dat->indx[n];
271        break;
272      }
273    }
274  } else {                      /* Binary search of the index */
275    int left = 0;
276    int cmp;
277    int right = help_dat->entries - 1;
278
279    while (1) {
280      int n = (left + right) / 2;
281
282      if (left > right)
283        break;
284
285      cmp = strcasecmp(the_topic, help_dat->indx[n].topic);
286
287      if (cmp == 0) {
288        entry = &help_dat->indx[n];
289        break;
290      } else if (cmp < 0) {
291        /* We need to catch the first prefix */
292        if (string_prefix(help_dat->indx[n].topic, the_topic)) {
293          int m;
294          for (m = n - 1; m >= 0; m--) {
295            if (!string_prefix(help_dat->indx[m].topic, the_topic))
296              break;
297          }
298          entry = &help_dat->indx[m + 1];
299          break;
300        }
301        if (left == right)
302          break;
303        right = n - 1;
304      } else {                  /* cmp > 0 */
305        if (left == right)
306          break;
307        left = n + 1;
308      }
309    }
310  }
311  return entry;
312}
313
314static void
315write_topic(long int p)
316{
317  tlist *cur, *nextptr;
318  help_indx *temp;
319  for (cur = top; cur; cur = nextptr) {
320    nextptr = cur->next;
321    if (num_topics >= top_topics) {
322      top_topics += top_topics / 2 + 20;
323      if (topics)
324        topics = (help_indx *) realloc(topics, top_topics * sizeof(help_indx));
325      else
326        topics = (help_indx *) malloc(top_topics * sizeof(help_indx));
327      if (!topics) {
328        mush_panic(T("Out of memory"));
329      }
330    }
331    temp = &topics[num_topics++];
332    temp->pos = p;
333    strcpy(temp->topic, cur->topic);
334    free(cur);
335  }
336  top = NULL;
337}
338
339static int WIN32_CDECL topic_cmp(const void *s1, const void *s2);
340static int WIN32_CDECL
341topic_cmp(const void *s1, const void *s2)
342{
343  const help_indx *a = s1;
344  const help_indx *b = s2;
345
346  return strcasecmp(a->topic, b->topic);
347
348}
349
350static void
351help_build_index(help_file *h, int restricted)
352{
353  long bigpos, pos = 0;
354  bool in_topic;
355  int i, lineno, ntopics;
356  size_t n;
357  char *s, *topic;
358  char the_topic[TOPIC_NAME_LEN + 1];
359  char line[LINE_SIZE + 1];
360  FILE *rfp;
361  tlist *cur;
362
363  /* Quietly ignore null values for the file */
364  if (!h || !h->file)
365    return;
366  if ((rfp = fopen(h->file, FOPEN_READ)) == NULL) {
367    do_rawlog(LT_ERR, T("Can't open %s for reading"), h->file);
368    return;
369  }
370
371  if (restricted)
372    do_rawlog(LT_WIZ, T("Indexing file %s (admin topics)"), h->file);
373  else
374    do_rawlog(LT_WIZ, T("Indexing file %s"), h->file);
375  topics = NULL;
376  num_topics = 0;
377  top_topics = 0;
378  bigpos = 0L;
379  lineno = 0;
380  ntopics = 0;
381
382  in_topic = 0;
383
384  while (fgets(line, LINE_SIZE, rfp) != NULL) {
385    ++lineno;
386    if (ntopics == 0) {
387      /* Looking for the first topic, but we'll ignore blank lines */
388      if (!line[0]) {
389        /* Someone's feeding us /dev/null? */
390        do_rawlog(LT_ERR, T("Malformed help file %s doesn't start with &"),
391                  h->file);
392        fclose(rfp);
393        return;
394      }
395      if (isspace((unsigned char) line[0]))
396        continue;
397      if (line[0] != '&') {
398        do_rawlog(LT_ERR, T("Malformed help file %s doesn't start with &"),
399                  h->file);
400        fclose(rfp);
401        return;
402      }
403    }
404    n = strlen(line);
405    if (line[n - 1] != '\n') {
406      do_rawlog(LT_ERR, T("Line %d of %s: line too long"), lineno, h->file);
407    }
408    if (line[0] == '&') {
409      ++ntopics;
410      if (!in_topic) {
411        /* Finish up last entry */
412        if (ntopics > 1) {
413          write_topic(pos);
414        }
415        in_topic = true;
416      }
417      /* parse out the topic */
418      /* Get the beginning of the topic string */
419      for (topic = &line[1];
420           (*topic == ' ' || *topic == '\t') && *topic != '\0'; topic++) ;
421
422      /* Get the topic */
423      strcpy(the_topic, "");
424      for (i = -1, s = topic; *s != '\n' && *s != '\0'; s++) {
425        if (i >= TOPIC_NAME_LEN - 1)
426          break;
427        if (*s != ' ' || the_topic[i] != ' ')
428          the_topic[++i] = *s;
429      }
430      if ((restricted && the_topic[0] == '&')
431          || (!restricted && the_topic[0] != '&')) {
432        the_topic[++i] = '\0';
433        cur = (tlist *) malloc(sizeof(tlist));
434        strcpy(cur->topic, the_topic);
435        cur->next = top;
436        top = cur;
437      }
438    } else {
439      if (in_topic) {
440        pos = bigpos;
441      }
442      in_topic = false;
443    }
444    bigpos = ftell(rfp);
445  }
446
447  /* Handle last topic */
448  write_topic(pos);
449  qsort(topics, num_topics, sizeof(help_indx), topic_cmp);
450  h->entries = num_topics;
451  h->indx = topics;
452  add_check("help_index");
453  fclose(rfp);
454  do_rawlog(LT_WIZ, T("%d topics indexed."), num_topics);
455  return;
456}
457
458/* ARGSUSED */
459FUNCTION(fun_textfile)
460{
461  help_file *h;
462
463  h = hashfind(strupper(args[0]), &help_files);
464  if (!h) {
465    safe_str(T("#-1 NO SUCH FILE"), buff, bp);
466    return;
467  }
468  if (h->admin && !Hasprivs(executor)) {
469    safe_str(T(e_perm), buff, bp);
470    return;
471  }
472
473  if (wildcard(args[1])) {
474    char **entries;
475    int len = 0;
476    entries = list_matching_entries(args[1], h, &len);
477    if (len == 0)
478      safe_str(T("No matching help topics."), buff, bp);
479    else
480      arr2list(entries, len, buff, bp, ", ");
481    free_entry_list(entries);
482  } else
483    safe_str(string_spitfile(h, args[1]), buff, bp);
484}
485
486/* ARGSUSED */
487FUNCTION(fun_textentries)
488{
489  help_file *h;
490  char **entries;
491  int len = 0;
492  const char *sep = " ";
493
494  h = hashfind(strupper(args[0]), &help_files);
495  if (!h) {
496    safe_str(T("#-1 NO SUCH FILE"), buff, bp);
497    return;
498  }
499  if (h->admin && !Hasprivs(executor)) {
500    safe_str(T(e_perm), buff, bp);
501    return;
502  }
503  if (nargs > 2)
504    sep = args[2];
505
506  entries = list_matching_entries(args[1], h, &len);
507  if (entries) {
508    arr2list(entries, len, buff, bp, sep);
509    free_entry_list(entries);
510  }
511}
512
513static const char *
514normalize_entry(help_file *help_dat, const char *arg1)
515{
516  static char the_topic[LINE_SIZE + 2];
517
518  if (*arg1 == '\0')
519    arg1 = (char *) "help";
520  else if (*arg1 == '&')
521    return T("#-1 INVALID ENTRY");
522  if (help_dat->admin)
523    snprintf(the_topic, LINE_SIZE, "&%s", arg1);
524  else
525    mush_strncpy(the_topic, arg1, LINE_SIZE);
526  return the_topic;
527}
528
529static const char *
530string_spitfile(help_file *help_dat, char *arg1)
531{
532  help_indx *entry = NULL;
533  FILE *fp;
534  char line[LINE_SIZE + 1];
535  char the_topic[LINE_SIZE + 2];
536  size_t n;
537  static char buff[BUFFER_LEN];
538  char *bp;
539
540  strcpy(the_topic, normalize_entry(help_dat, arg1));
541
542  if (!help_dat->indx || help_dat->entries == 0)
543    return T("#-1 NO INDEX FOR FILE");
544
545  entry = help_find_entry(help_dat, the_topic);
546  if (!entry) {
547    return T("#-1 NO ENTRY");
548  }
549
550  if ((fp = fopen(help_dat->file, FOPEN_READ)) == NULL) {
551    return T("#-1 UNAVAILABLE");
552  }
553  if (fseek(fp, entry->pos, 0) < 0L) {
554    return T("#-1 UNAVAILABLE");
555  }
556  bp = buff;
557  for (n = 0; n < BUFFER_LEN; n++) {
558    if (fgets(line, LINE_SIZE, fp) == NULL)
559      break;
560    if (line[0] == '&')
561      break;
562    safe_str(line, buff, &bp);
563  }
564  *bp = '\0';
565  fclose(fp);
566  return buff;
567}
568
569/** Return a string with all help entries that match a pattern */
570static char **
571list_matching_entries(const char *pattern, help_file *help_dat, int *len)
572{
573  char **buff;
574  int offset;
575  size_t n;
576
577  if (help_dat->admin)
578    offset = 1;                 /* To skip the leading & */
579  else
580    offset = 0;
581
582  if (!wildcard(pattern)) {
583    /* Quick way out, use the other kind of matching */
584    char the_topic[LINE_SIZE + 2];
585    help_indx *entry = NULL;
586    strcpy(the_topic, normalize_entry(help_dat, pattern));
587    if (!help_dat->indx || help_dat->entries == 0) {
588      *len = 0;
589      return NULL;
590    }
591    entry = help_find_entry(help_dat, the_topic);
592    if (!entry) {
593      *len = 0;
594      return NULL;
595    } else {
596      *len = 1;
597      buff = mush_malloc(sizeof(char **), "help.search");
598      *buff = entry->topic + offset;
599      return buff;
600    }
601  }
602
603  buff = mush_calloc(help_dat->entries, sizeof(char *), "help.search");
604  *len = 0;
605
606  for (n = 0; n < help_dat->entries; n++)
607    if (quick_wild(pattern, help_dat->indx[n].topic + offset)) {
608      buff[*len] = help_dat->indx[n].topic + offset;
609      *len += 1;
610    }
611
612  return buff;
613}
614
615static void
616free_entry_list(char **entries)
617{
618  if (entries)
619    mush_free(entries, "help.search");
620}
Note: See TracBrowser for help on using the browser.