source: git/src/message.c @ c1cf79d

RELEASE/1.0RELEASE/1.1RELEASE/1.2debug-cidebug-ci-sanitisersfaster-cavernloglog-selectstereostereo-2025walls-datawalls-data-hanging-as-warningwarn-only-for-hanging-survey
Last change on this file since c1cf79d was 2163157, checked in by Olly Betts <olly@…>, 24 years ago

Fixes for clean compilation on RISC OS.

git-svn-id: file:///home/survex-svn/survex/trunk@952 4b37db11-9a0c-4f06-9ece-9ab7cdaee568

  • Property mode set to 100644
File size: 18.0 KB
Line 
1/* > message.c
2 * Fairly general purpose message and error routines
3 * Copyright (C) 1993-2001 Olly Betts
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 */
19
20/*#define DEBUG 1*/
21
22#ifdef HAVE_CONFIG_H
23# include <config.h>
24#endif
25
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29#include <ctype.h>
30#include <limits.h>
31#include <errno.h>
32#include <locale.h>
33
34#include "whichos.h"
35#include "filename.h"
36#include "message.h"
37#include "osdepend.h"
38#include "filelist.h"
39#include "debug.h"
40
41#ifdef HAVE_SIGNAL
42# ifdef HAVE_SETJMP
43#  include <setjmp.h>
44static jmp_buf jmpbufSignal;
45#  include <signal.h>
46# else
47#  undef HAVE_SIGNAL
48# endif
49#endif
50
51#if (OS==WIN32)
52#include <windows.h>
53#endif
54
55#if (OS==RISCOS)
56#include "oslib/wimpreadsy.h"
57#endif
58
59/* This is the name of the default language.  Add -DDEFAULTLANG to CFLAGS
60 * e.g. with `CFLAGS="-DDEFAULTLANG=fr" ./configure'
61 */
62#ifndef DEFAULTLANG
63# define DEFAULTLANG "en"
64#endif
65
66/* For funcs which want to be immune from messing around with different
67 * calling conventions */
68#ifndef CDECL
69# define CDECL
70#endif
71
72int msg_warnings = 0; /* keep track of how many warnings we've given */
73int msg_errors = 0;   /* and how many (non-fatal) errors */
74
75/* in case osmalloc() fails before szAppNameCopy is set up */
76static const char *szAppNameCopy = "anonymous program";
77
78/* error code for failed osmalloc and osrealloc calls */
79static void
80outofmem(OSSIZE_T size)
81{
82   fatalerror(/*Out of memory (couldn't find %lu bytes).*/1,
83              (unsigned long)size);
84}
85
86#ifdef TOMBSTONES
87#define TOMBSTONE_SIZE 16
88static const char tombstone[TOMBSTONE_SIZE] = "012345\xfftombstone";
89#endif
90
91/* malloc with error catching if it fails. Also allows us to write special
92 * versions easily eg for DOS EMS or MS Windows.
93 */
94void FAR *
95osmalloc(OSSIZE_T size)
96{
97   void FAR *p;
98#ifdef TOMBSTONES
99   size += TOMBSTONE_SIZE * 2;
100   p = malloc(size);
101#else
102   p = xosmalloc(size);
103#endif
104   if (p == NULL) outofmem(size);
105#ifdef TOMBSTONES
106   printf("osmalloc truep=%p truesize=%d\n", p, size);
107   memcpy(p, tombstone, TOMBSTONE_SIZE);
108   memcpy(p + size - TOMBSTONE_SIZE, tombstone, TOMBSTONE_SIZE);
109   *(size_t *)p = size;
110   p += TOMBSTONE_SIZE;
111#endif
112   return p;
113}
114
115/* realloc with error catching if it fails. */
116void FAR *
117osrealloc(void *p, OSSIZE_T size)
118{
119   /* some pre-ANSI realloc implementations don't cope with a NULL pointer */
120   if (p == NULL) {
121      p = xosmalloc(size);
122   } else {
123#ifdef TOMBSTONES
124      int true_size;
125      size += TOMBSTONE_SIZE * 2;
126      p -= TOMBSTONE_SIZE;
127      true_size = *(size_t *)p;
128      printf("osrealloc (in truep=%p truesize=%d)\n", p, true_size);
129      if (memcmp(p + sizeof(size_t), tombstone + sizeof(size_t),
130                 TOMBSTONE_SIZE - sizeof(size_t)) != 0) {
131         printf("start tombstone for block %p, size %d corrupted!",
132                p + TOMBSTONE_SIZE, true_size - TOMBSTONE_SIZE * 2);
133      }
134      if (memcmp(p + true_size - TOMBSTONE_SIZE, tombstone,
135                 TOMBSTONE_SIZE) != 0) {
136         printf("end tombstone for block %p, size %d corrupted!",
137                p + TOMBSTONE_SIZE, true_size - TOMBSTONE_SIZE * 2);
138      }
139      p = realloc(p, size);
140      if (p == NULL) outofmem(size);
141      printf("osrealloc truep=%p truesize=%d\n", p, size);
142      memcpy(p, tombstone, TOMBSTONE_SIZE);
143      memcpy(p + size - TOMBSTONE_SIZE, tombstone, TOMBSTONE_SIZE);
144      *(size_t *)p = size;
145      p += TOMBSTONE_SIZE;
146#else
147      p = xosrealloc(p, size);
148#endif
149   }
150   if (p == NULL) outofmem(size);
151   return p;
152}
153
154char FAR *
155osstrdup(const char *str)
156{
157   char *p;
158   OSSIZE_T len;
159   len = strlen(str) + 1;
160   p = osmalloc(len);
161   memcpy(p, str, len);
162   return p;
163}
164
165/* osfree is usually just a macro in osalloc.h */
166#ifdef TOMBSTONES
167void
168osfree(void *p)
169{
170   int true_size;
171   if (!p) return;
172   p -= TOMBSTONE_SIZE;
173   true_size = *(size_t *)p;
174   printf("osfree truep=%p truesize=%d\n", p, true_size);
175   if (memcmp(p + sizeof(size_t), tombstone + sizeof(size_t),
176              TOMBSTONE_SIZE - sizeof(size_t)) != 0) {
177      printf("start tombstone for block %p, size %d corrupted!",
178             p + TOMBSTONE_SIZE, true_size - TOMBSTONE_SIZE * 2);
179   }
180   if (memcmp(p + true_size - TOMBSTONE_SIZE, tombstone,
181              TOMBSTONE_SIZE) != 0) {
182      printf("end tombstone for block %p, size %d corrupted!",
183             p + TOMBSTONE_SIZE, true_size - TOMBSTONE_SIZE * 2);
184   }
185   free(p);
186}
187#endif
188
189#ifdef HAVE_SIGNAL
190
191static int sigReceived;
192
193/* for systems not using autoconf, assume the signal handler returns void
194 * unless specified elsewhere */
195#ifndef RETSIGTYPE
196# define RETSIGTYPE void
197#endif
198
199static CDECL RETSIGTYPE FAR
200report_sig(int sig)
201{
202   sigReceived = sig;
203   longjmp(jmpbufSignal, 1);
204}
205
206static void
207init_signals(void)
208{
209   int en;
210   if (!setjmp(jmpbufSignal)) {
211#if 1 /* disable these to get a core dump */
212      signal(SIGABRT, report_sig); /* abnormal termination eg abort() */
213      signal(SIGFPE,  report_sig); /* arithmetic error eg /0 or overflow */
214      signal(SIGILL,  report_sig); /* illegal function image eg illegal instruction */
215      signal(SIGSEGV, report_sig); /* illegal storage access eg access outside memory limits */
216#endif
217# ifdef SIGSTAK /* only on RISC OS AFAIK */
218      signal(SIGSTAK, report_sig); /* stack overflow */
219# endif
220      return;
221   }
222
223   switch (sigReceived) {
224      case SIGABRT: en = /*Abnormal termination*/90; break;
225      case SIGFPE:  en = /*Arithmetic error*/91; break;
226      case SIGILL:  en = /*Illegal instruction*/92; break;
227      case SIGSEGV: en = /*Bad memory access*/94; break;
228# ifdef SIGSTAK
229      case SIGSTAK: en = /*Stack overflow*/96; break;
230# endif
231      default:      en = /*Unknown signal received*/97; break;
232   }
233   fputsnl(msg(en), STDERR);
234
235   /* Any of the signals we catch indicates a bug */
236   fatalerror(/*Bug in program detected! Please report this to the authors*/11);
237
238   exit(EXIT_FAILURE);
239}
240#endif
241
242static int
243default_charset(void)
244{
245#if (OS==RISCOS)
246   /* RISCOS 3.1 and above CHARSET_RISCOS31 (ISO_8859_1 + extras in 128-159)
247    * RISCOS < 3.1 is ISO_8859_1 */
248   int version;
249   if (xwimpreadsysinfo_version(&version) != NULL) {
250      /* RISC OS 2 or some error (don't care which) */
251      return CHARSET_ISO_8859_1;
252   }
253
254   /* oddly wimp_VERSION_RO3 is RISC OS 3.1 */
255   if (version < wimp_VERSION_RO3) return CHARSET_ISO_8859_1;
256
257   return CHARSET_RISCOS31;
258#elif (OS==MSDOS)
259   return CHARSET_DOSCP850;
260#else
261   /* FIXME: assume ISO_8859_1 for now */
262   return CHARSET_ISO_8859_1;
263#endif
264}
265
266#if (OS==MSDOS)
267static int
268xlate_dos_cp850(int unicode)
269{
270   switch (unicode) {
271#include "uni2dos.h"
272   }
273   return 0;
274}
275#endif
276
277static int
278add_unicode(int charset, unsigned char *p, int value)
279{
280#ifdef DEBUG
281   fprintf(stderr, "add_unicode(%d, %p, %d)\n", charset, p, value);
282#endif
283   if (value == 0) return 0;
284   switch (charset) {
285   case CHARSET_USASCII:
286      if (value < 0x80) {
287         *p = value;
288         return 1;
289      }
290      break;
291   case CHARSET_ISO_8859_1:
292      if (value < 0x100) {
293         *p = value;
294         return 1;
295      }
296      break;
297#if (OS==RISCOS)
298   case CHARSET_RISCOS31:
299      /* RISC OS 3.1 (and later) extensions to ISO-8859-1 */
300      switch (value) {
301       case 0x152: value = 0x9a; break; /* &OElig; */
302       case 0x153: value = 0x9b; break; /* &oelig; */
303       case 0x174: value = 0x81; break; /* &Wcirc; */
304       case 0x175: value = 0x82; break; /* &wcirc; */
305       case 0x176: value = 0x85; break; /* &Ycirc; */
306       case 0x177: value = 0x86; break; /* &ycirc; */
307      }
308      if (value < 0x100) {
309         *p = value;
310         return 1;
311      }
312      break;
313#endif
314#if (OS==MSDOS)
315   case CHARSET_DOSCP850:
316      value = xlate_dos_cp850(value);
317      if (value) {
318         *p = value;
319         return 1;
320      }
321      break;
322#endif
323   }
324   return 0;
325}
326
327/* fall back on looking in the current directory */
328static const char *pth_cfg_files = "";
329
330static int num_msgs = 0;
331static char **msg_array = NULL;
332
333const char *msg_lang = NULL;
334const char *msg_lang2 = NULL;
335
336static void
337parse_msg_file(int charset_code)
338{
339   FILE *fh;
340   unsigned char header[20];
341   int i;
342   unsigned len;
343   unsigned char *p;
344   char *fnm, *s;
345
346#ifdef DEBUG
347   fprintf(stderr, "parse_msg_file(%d)\n", charset_code);
348#endif
349
350   fnm = osstrdup(msg_lang);
351   /* trim off charset from stuff like "de_DE.iso8859_1" */
352   s = strchr(fnm, '.');
353   if (s) *s = '\0';
354
355   fh = fopenWithPthAndExt(pth_cfg_files, fnm, EXT_SVX_MSG, "rb", NULL);
356
357   if (!fh) {
358      /* e.g. if 'en-COCKNEY' is unknown, see if we know 'en' */
359      if (strlen(fnm) > 3 && fnm[2] == '-') {
360         fnm[2] = '\0';
361         fh = fopenWithPthAndExt(pth_cfg_files, fnm, EXT_SVX_MSG, "rb", NULL);
362         if (!fh) fnm[2] = '-'; /* for error reporting */
363      }
364   }
365
366   if (!fh) {
367      fatalerror(/*Can't open message file `%s' using path `%s'*/1000,
368                 fnm, pth_cfg_files);
369   }
370
371   if (fread(header, 1, 20, fh) < 20 ||
372       memcmp(header, "Svx\nMsg\r\n\xfe\xff", 12) != 0) {
373      fatalerror(/*Problem with message file `%s'*/1001, fnm);
374   }
375
376   if (header[12] != 0)
377      fatalerror(/*I don't understand this message file version*/1002);
378
379   num_msgs = (header[14] << 8) | header[15];
380
381   len = 0;
382   for (i = 16; i < 20; i++) len = (len << 8) | header[i];
383
384   p = osmalloc(len);
385   if (fread(p, 1, len, fh) < len)
386      fatalerror(/*Message file truncated?*/1003);
387
388   fclose(fh);
389
390#ifdef DEBUG
391   fprintf(stderr, "fnm = `%s', num_msgs = %d, len = %d\n", fnm, num_msgs, len);
392#endif
393   osfree(fnm);
394
395   msg_array = osmalloc(sizeof(char *) * num_msgs);
396
397   for (i = 0; i < num_msgs; i++) {
398      unsigned char *to = p;
399      int ch;
400      msg_array[i] = (char *)p;
401
402      /* If we want UTF8 anyway, we just need to find the start of each
403       * message */
404      if (charset_code == CHARSET_UTF8) {
405         p += strlen((char *)p) + 1;
406         continue;
407      }
408
409      while ((ch = *p++) != 0) {
410         /* A byte in the range 0x80-0xbf or 0xf0-0xff isn't valid in
411          * this state, (0xf0-0xfd mean values > 0xffff) so treat as
412          * literal and try to resync so we cope better when fed
413          * non-utf-8 data.  Similarly we abandon a multibyte sequence
414          * if we hit an invalid character. */
415         if (ch >= 0xc0 && ch < 0xf0) {
416            int ch1 = *p;
417            if ((ch1 & 0xc0) != 0x80) goto resync;
418
419            if (ch < 0xe0) {
420               /* 2 byte sequence */
421               ch = ((ch & 0x1f) << 6) | (ch1 & 0x3f);
422               p++;
423            } else {
424               /* 3 byte sequence */
425               int ch2 = p[1];
426               if ((ch2 & 0xc0) != 0x80) goto resync;
427               ch = ((ch & 0x1f) << 12) | ((ch1 & 0x3f) << 6) | (ch2 & 0x3f);
428               p += 2;
429            }
430         }
431
432         resync:
433
434         if (ch < 127) {
435            *to++ = (char)ch;
436         } else {
437            /* FIXME: this rather assumes a 2 byte UTF-8 code never
438             * transliterates to more than 2 characters */
439            to += add_unicode(charset_code, to, ch);
440         }
441      }
442      *to++ = '\0';
443   }
444}
445
446const char *
447msg_cfgpth(void)
448{
449   return pth_cfg_files;
450}
451
452void
453msg_init(const char *argv0)
454{
455   char *p;
456
457#ifdef HAVE_SIGNAL
458   init_signals();
459#endif
460   /* Point to argv0 itself so we report a more helpful error if the code to work
461    * out the clean appname generates a signal */
462   szAppNameCopy = argv0;
463#if (OS == UNIX)
464   /* use name as-is on Unix - programs run from path get name as supplied */
465   szAppNameCopy = osstrdup(argv0);
466#else
467   /* use the lower-cased leafname on other platforms */
468   szAppNameCopy = p = leaf_from_fnm(argv0);
469   while (*p) {
470      *p = tolower(*p);
471      p++;
472   }
473#endif
474
475   /* Look for env. var. "SURVEXHOME" or the like */
476   p = getenv("SURVEXHOME");
477   if (p && *p) {
478      pth_cfg_files = osstrdup(p);
479#if (OS==UNIX) && defined(DATADIR) && defined(PACKAGE)
480   } else {
481      /* under Unix, we compile in the configured path */
482      pth_cfg_files = DATADIR "/" PACKAGE;
483#else
484   } else if (argv0) {
485      /* else try the path on argv[0] */
486      pth_cfg_files = path_from_fnm(argv0);
487#endif
488   }
489
490   msg_lang = getenv("SURVEXLANG");
491#ifdef DEBUG
492   fprintf(stderr, "msg_lang = %p (= \"%s\")\n", msg_lang, msg_lang?msg_lang:"(null)");
493#endif
494
495   if (!msg_lang || !*msg_lang) {
496      msg_lang = getenv("LANG");
497      if (!msg_lang || !*msg_lang) {
498#if (OS==WIN32)
499         LCID locid;
500#endif
501         msg_lang = DEFAULTLANG;
502#if (OS==WIN32)
503         locid = GetUserDefaultLCID();
504         if (locid) {
505            WORD langid = LANGIDFROMLCID(locid);
506            switch (PRIMARYLANGID(langid)) {
507             case LANG_CATALAN:
508               msg_lang = "ca";
509               break;
510             case LANG_ENGLISH:
511               if (SUBLANGID(langid) == SUBLANG_ENGLISH_US)
512                  msg_lang = "en_US";
513               else
514                  msg_lang = "en";
515               break;
516             case LANG_FRENCH:
517               msg_lang = "fr";
518               break;
519             case LANG_GERMAN:
520               switch (SUBLANGID(langid)) {
521                case SUBLANG_GERMAN_SWISS:
522                  msg_lang = "de_CH";
523                  break;
524                case SUBLANG_GERMAN:
525                  msg_lang = "de_DE";
526                  break;
527                default:
528                  msg_lang = "de";
529               }
530               break;
531             case LANG_ITALIAN:
532               msg_lang = "it";
533               break;
534             case LANG_PORTUGUESE:
535               if (SUBLANGID(langid) == SUBLANG_PORTUGUESE_BRAZILIAN)
536                  msg_lang = "pt_BR";
537               else
538                  msg_lang = "pt";
539               break;
540             case LANG_SPANISH:
541               msg_lang = "es";
542               break;
543            }
544         }
545#endif
546      }
547   }
548#ifdef DEBUG
549   fprintf(stderr, "msg_lang = %p (= \"%s\")\n", msg_lang, msg_lang?msg_lang:"(null)");
550#endif
551
552   /* On Mandrake LANG defaults to C */
553   if (strcmp(msg_lang, "C") == 0) msg_lang = "en";
554
555   msg_lang = osstrdup(msg_lang);
556
557   /* Convert en-us to en_US, etc */
558   p = strchr(msg_lang, '-');
559   if (p) {
560      *p++ = '_';
561      while (*p) {
562         *p = toupper(*p);
563         p++;
564      }
565   }
566
567   p = strchr(msg_lang, '_');
568   if (p) {
569      *p = '\0';
570      msg_lang2 = osstrdup(msg_lang);
571      *p = '_';
572   }
573
574#ifdef LC_MESSAGES
575   /* try to setlocale() appropriately too */
576   if (!setlocale(LC_MESSAGES, msg_lang)) {
577      if (msg_lang2) setlocale(LC_MESSAGES, msg_lang2);
578   }
579#endif
580
581   select_charset(default_charset());
582}
583
584/* no point extracting these errors as they won't get used if file opens */
585/* FIXME: if DEFAULTLANG != "en" translate these... */
586static const char *dontextract[] = {
587   "Can't open message file `%s' using path `%s'", /*1000*/
588   "Problem with message file `%s'", /*1001*/
589   "I don't understand this message file version", /*1002*/
590   "Message file truncated?" /*1003*/
591};
592
593/* message may be overwritten by next call
594 * (but not in current implementation) */
595const char *
596msg(int en)
597{
598   /* NB can't use ASSERT here! */
599   static char badbuf[256];
600   if (en >= 1000 && en < 1000 + (int)(sizeof(dontextract)/sizeof(char*)))
601      return dontextract[en - 1000];
602   if (!msg_array) {
603      if (en != 1)  {
604         sprintf(badbuf, "Message %d requested before msg_array initialised\n", en);
605         return badbuf;
606      }
607      /* this should be the only message which can be requested before
608       * the message file is opened and read... */
609      return "Out of memory (couldn't find %ul bytes).\n";
610   }
611
612   if (en < 0 || en >= num_msgs) {
613      sprintf(badbuf, "Message %d out of range\n", en);
614      return badbuf;
615   }
616
617   return msg_array[en];
618}
619
620/* returns persistent copy of message */
621const char *
622msgPerm(int en)
623{
624   return msg(en);
625}
626
627void
628v_report(int severity, const char *fnm, int line, int en, va_list ap)
629{
630#ifdef AVEN
631   extern void aven_v_report(int severity, const char *fnm, int line, int en,
632                             va_list ap);
633   aven_v_report(severity, fnm, line, en, ap);
634#else
635   if (fnm) {
636      fputs(fnm, STDERR);
637      if (line) fprintf(STDERR, ":%d", line);
638   } else {
639      fputs(szAppNameCopy, STDERR);
640   }
641   fputs(": ", STDERR);
642
643   if (severity == 0) {
644      fputs(msg(/*warning*/4), STDERR);
645      fputs(": ", STDERR);
646   }
647
648   vfprintf(STDERR, msg(en), ap);
649   fputnl(STDERR);
650#endif
651
652   switch (severity) {
653    case 0:
654      msg_warnings++;
655      break;
656    case 1:
657      msg_errors++;
658      if (msg_errors == 50)
659         fatalerror_in_file(fnm, 0, /*Too many errors - giving up*/19);
660      break;
661    case 2:
662      exit(EXIT_FAILURE);
663   }
664}
665
666void
667warning(int en, ...)
668{
669   va_list ap;
670   va_start(ap, en);
671   v_report(0, NULL, 0, en, ap);
672   va_end(ap);
673}
674
675void
676error(int en, ...)
677{
678   va_list ap;
679   va_start(ap, en);
680   v_report(1, NULL, 0, en, ap);
681   va_end(ap);
682}
683
684void
685fatalerror(int en, ...)
686{
687   va_list ap;
688   va_start(ap, en);
689   v_report(2, NULL, 0, en, ap);
690   va_end(ap);
691}
692
693void
694warning_in_file(const char *fnm, int line, int en, ...)
695{
696   va_list ap;
697   va_start(ap, en);
698   v_report(0, fnm, line, en, ap);
699   va_end(ap);
700}
701
702void
703error_in_file(const char *fnm, int line, int en, ...)
704{
705   va_list ap;
706   va_start(ap, en);
707   v_report(1, fnm, line, en, ap);
708   va_end(ap);
709}
710
711void
712fatalerror_in_file(const char *fnm, int line, int en, ...)
713{
714   va_list ap;
715   va_start(ap, en);
716   v_report(2, fnm, line, en, ap);
717   va_end(ap);
718}
719
720/* Code to support switching character set at runtime (e.g. for a printer
721 * driver to support different character sets on screen and on the printer)
722 */
723typedef struct charset_li {
724   struct charset_li *next;
725   int code;
726   char **msg_array;
727} charset_li;
728
729static charset_li *charset_head = NULL;
730
731static int charset = CHARSET_BAD;
732
733int
734select_charset(int charset_code)
735{
736   int old_charset = charset;
737   charset_li *p;
738
739#ifdef DEBUG
740   fprintf(stderr, "select_charset(%d), old charset = %d\n", charset_code,
741           charset);
742#endif
743
744   charset = charset_code;
745
746   /* check if we've already parsed messages for new charset */
747   for (p = charset_head; p; p = p->next) {
748#ifdef DEBUG
749      printf("%p: code %d msg_array %p\n", p, p->code, p->msg_array);
750#endif
751      if (p->code == charset) {
752         msg_array = p->msg_array;
753         return old_charset;
754      }
755   }
756
757   /* nope, got to reparse message file */
758   parse_msg_file(charset_code);
759
760   /* add to list */
761   p = osnew(charset_li);
762   p->code = charset;
763   p->msg_array = msg_array;
764   p->next = charset_head;
765   charset_head = p;
766
767   return old_charset;
768}
Note: See TracBrowser for help on using the repository browser.