1 /* gpg-error.c - Determining gpg-error error codes.
2    Copyright (C) 2004, 2016 g10 Code GmbH
3 
4    This file is part of libgpg-error.
5 
6    libgpg-error is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Lesser General Public License
8    as published by the Free Software Foundation; either version 2.1 of
9    the License, or (at your option) any later version.
10 
11    libgpg-error is distributed in the hope that it will be useful, but
12    WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14    Lesser General Public License for more details.
15 
16    You should have received a copy of the GNU Lesser General Public
17    License along with libgpg-error; if not, write to the Free
18    Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19    02111-1307, USA.  */
20 
21 #if HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24 
25 #include <stddef.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <errno.h>
29 #include <limits.h>
30 #include <stdio.h>
31 
32 #ifdef HAVE_LOCALE_H
33 # include <locale.h>
34 #endif
35 #ifdef ENABLE_NLS
36 #ifdef HAVE_W32_SYSTEM
37 # include "gettext.h"
38 #else
39 # include <libintl.h>
40 #endif
41 # define _(a) gettext (a)
42 # ifdef gettext_noop
43 #  define N_(a) gettext_noop (a)
44 # else
45 #  define N_(a) (a)
46 # endif
47 #else
48 # define _(a) (a)
49 # define N_(a) (a)
50 #endif
51 
52 #include <gpg-error.h>
53 
54 
55 #if HAVE_W32_SYSTEM
56 /* The implementation follows below.  */
57 static char *get_locale_dir (void);
58 static void drop_locale_dir (char *locale_dir);
59 #else
60 #define get_locale_dir() LOCALEDIR
61 #define drop_locale_dir(dir)
62 #endif
63 
64 static void
i18n_init(void)65 i18n_init (void)
66 {
67 #ifdef ENABLE_NLS
68   char *locale_dir;
69 
70 #ifdef HAVE_LC_MESSAGES
71   setlocale (LC_TIME, "");
72   setlocale (LC_MESSAGES, "");
73 #else
74 # ifndef HAVE_W32_SYSTEM
75   setlocale (LC_ALL, "" );
76 # endif
77 #endif
78 
79   /* Note that for this program we would only need the textdomain call
80      because libgpg-error already initializes itself to its locale dir
81      (via gpg_err_init or a constructor).  However this is only done
82      for the static standard locale and thus if the above setlocale
83      calls select a different locale the bindtext below will do
84      something else.  */
85 
86   locale_dir = get_locale_dir ();
87   if (locale_dir)
88     {
89       bindtextdomain (PACKAGE, locale_dir);
90       drop_locale_dir (locale_dir);
91     }
92   textdomain (PACKAGE);
93 #endif
94 }
95 
96 
97 #ifdef HAVE_W32_SYSTEM
98 
99 #include <windows.h>
100 
101 
102 static char *
get_locale_dir(void)103 get_locale_dir (void)
104 {
105   static wchar_t moddir[MAX_PATH+5];
106   char *result, *p;
107   int nbytes;
108 
109   if (!GetModuleFileNameW (NULL, moddir, MAX_PATH))
110     *moddir = 0;
111 
112 #define SLDIR "\\share\\locale"
113   if (*moddir)
114     {
115       nbytes = WideCharToMultiByte (CP_UTF8, 0, moddir, -1, NULL, 0, NULL, NULL);
116       if (nbytes < 0)
117         return NULL;
118 
119       result = malloc (nbytes + strlen (SLDIR) + 1);
120       if (result)
121         {
122           nbytes = WideCharToMultiByte (CP_UTF8, 0, moddir, -1,
123                                         result, nbytes, NULL, NULL);
124           if (nbytes < 0)
125             {
126               free (result);
127               result = NULL;
128             }
129           else
130             {
131               p = strrchr (result, '\\');
132               if (p)
133                 *p = 0;
134               /* If we are installed below "bin" strip that part and
135                  use the top directory instead.  */
136               p = strrchr (result, '\\');
137               if (p && !strcmp (p+1, "bin"))
138                 *p = 0;
139               /* Append the static part.  */
140               strcat (result, SLDIR);
141             }
142         }
143     }
144   else /* Use the old default value.  */
145     {
146       result = malloc (10 + strlen (SLDIR) + 1);
147       if (result)
148         {
149           strcpy (result, "c:\\gnupg");
150           strcat (result, SLDIR);
151         }
152     }
153 #undef SLDIR
154   return result;
155 }
156 
157 
158 static void
drop_locale_dir(char * locale_dir)159 drop_locale_dir (char *locale_dir)
160 {
161   free (locale_dir);
162 }
163 
164 #endif	/* HAVE_W32_SYSTEM */
165 
166 
167 const char *gpg_strerror_sym (gpg_error_t err);
168 const char *gpg_strsource_sym (gpg_error_t err);
169 
170 
171 /* Parse string STR assuming it is either a single number N or in the
172  * form K.N to denote an error source code K and and error code N.
173  * Returns false on error (e.g. invalid number) or true for valid
174  * codes; if true is returned a full error code is stored at ERR.  */
175 static int
get_err_from_number(char * str,gpg_error_t * err)176 get_err_from_number (char *str, gpg_error_t *err)
177 {
178   unsigned long nr;
179   char *tail;
180 
181   gpg_err_set_errno (0);
182   nr = strtoul (str, &tail, 0);
183   if (errno)
184     return 0;
185 
186   if (nr > UINT_MAX)
187     return 0;
188 
189   if (*tail)
190     {
191       unsigned long cnr = strtoul (tail + 1, &tail, 0);
192       if (errno || *tail)
193 	return 0;
194 
195       if (nr >= GPG_ERR_SOURCE_DIM || cnr >= GPG_ERR_CODE_DIM)
196 	return 0;
197 
198       nr = gpg_err_make (nr, cnr);
199     }
200 
201   *err = (unsigned int) nr;
202   return 1;
203 }
204 
205 
206 /* Helper function to parse a symbol either with a "GPG_ERR_SOURCE_"
207  * or "GPG_ERR_" prefix.  If the symbol is not available false is
208  * return; else the symbols value is ORed into the value at ERR
209  * (shifted for a GPG_ERR_SOURCE_) and true returned.  HAVE_SOURCE and
210  * HAVE_CODE are expected to be addresses where a 0 is stored; a 1 is
211  * stored at the respective address to mark whether a code or source
212  * value was found.  If one of those state variables already point to
213  * a true value the function will return 0 and not change the value at
214  * ERR.  */
215 static int
get_err_from_symbol_one(char * str,gpg_error_t * err,int * have_source,int * have_code)216 get_err_from_symbol_one (char *str, gpg_error_t *err,
217 			 int *have_source, int *have_code)
218 {
219   static const char src_prefix[] = "GPG_ERR_SOURCE_";
220   static const char code_prefix[] = "GPG_ERR_";
221 
222   if (!strncasecmp (src_prefix, str, sizeof (src_prefix) - 1))
223     {
224       gpg_err_source_t src;
225 
226       if (*have_source)
227 	return 0;
228       *have_source = 1;
229       str += sizeof (src_prefix) - 1;
230 
231       for (src = 0; src < GPG_ERR_SOURCE_DIM; src++)
232 	{
233 	  const char *src_sym;
234 
235 	  src_sym = gpg_strsource_sym (src << GPG_ERR_SOURCE_SHIFT);
236 	  if (src_sym && !strcasecmp (str, src_sym + sizeof (src_prefix) - 1))
237 	    {
238 	      *err |= src << GPG_ERR_SOURCE_SHIFT;
239 	      return 1;
240 	    }
241 	}
242     }
243   else if (!strncasecmp (code_prefix, str, sizeof (code_prefix) - 1))
244     {
245       gpg_err_code_t code;
246 
247       if (*have_code)
248 	return 0;
249       *have_code = 1;
250       str += sizeof (code_prefix) - 1;
251 
252       for (code = 0; code < GPG_ERR_CODE_DIM; code++)
253 	{
254 	  const char *code_sym = gpg_strerror_sym (code);
255 	  if (code_sym
256 	      && !strcasecmp (str, code_sym + sizeof (code_prefix) - 1))
257 	    {
258 	      *err |= code;
259 	      return 1;
260 	    }
261 	}
262     }
263   return 0;
264 }
265 
266 
267 /* Parse string STR assuming it is either a single symbol C or in the
268  * form S.C to denote an error source symbold S and and error code
269  * symbold C.  Returns false on error (e.g. invalid number) or true
270  * for valid codes; if true is returned a full error code is stored at
271  * ERR.  */
272 static int
get_err_from_symbol(char * str,gpg_error_t * err)273 get_err_from_symbol (char *str, gpg_error_t *err)
274 {
275   char *str2 = str;
276   int have_source = 0;
277   int have_code = 0;
278   int ret;
279   char *saved_pos = NULL;
280   char saved_char;
281 
282   *err = 0;
283   while (*str2 && ((*str2 >= 'A' && *str2 <= 'Z')
284 		   || (*str2 >= '0' && *str2 <= '9')
285 		   || *str2 == '_'))
286     str2++;
287   if (*str2)
288     {
289       saved_pos = str2;
290       saved_char = *str2;
291       *str2 = '\0';
292       str2++;
293     }
294   else
295     str2 = NULL;
296 
297   ret = get_err_from_symbol_one (str, err, &have_source, &have_code);
298   if (ret && str2)
299     ret = get_err_from_symbol_one (str2, err, &have_source, &have_code);
300 
301   if (saved_pos)
302     *saved_pos = saved_char;
303   return ret;
304 }
305 
306 
307 /* Parse string STR assuming it partial code symbol and store its
308  * value at ERR and return true.  */
309 static int
get_err_from_codesymbol(char * str,gpg_error_t * err)310 get_err_from_codesymbol (char *str, gpg_error_t *err)
311 {
312   static const char code_prefix[] = "GPG_ERR_";
313   gpg_err_code_t code;
314 
315   *err = 0;
316 
317   /* Skip an optional prefix.  */
318   if (!strncasecmp (code_prefix, str, sizeof (code_prefix) - 1))
319     str += sizeof (code_prefix) - 1;
320 
321   for (code = 0; code < GPG_ERR_CODE_DIM; code++)
322     {
323       const char *code_sym = gpg_strerror_sym (code);
324       if (code_sym
325           && !strcasecmp (str, code_sym + sizeof (code_prefix) - 1))
326         {
327           *err |= code;
328           return 1;
329         }
330     }
331   return 0;
332 }
333 
334 
335 /* Helper function to parse a string which maps back to a source or
336  * code value.  If no source or code for the symbold is available
337  * false is return; else the source or code value is ORed into the
338  * value at ERR (shifted for a GPG_ERR_SOURCE_) and true returned.
339  * The match is first tried on source values and then on code values.
340  * HAVE_SOURCE and HAVE_CODE are expected to be addresses where a 0 is
341  * stored; a 1 is stored at the respective address to mark whether a
342  * code or source value was found.  If one of those state variables
343  * already point to a true value the function will return 0 and not
344  * change the value at ERR.  */
345 static int
get_err_from_str_one(char * str,gpg_error_t * err,int * have_source,int * have_code)346 get_err_from_str_one (char *str, gpg_error_t *err,
347 		      int *have_source, int *have_code)
348 {
349   gpg_err_source_t src;
350   gpg_err_code_t code;
351 
352   for (src = 0; src < GPG_ERR_SOURCE_DIM; src++)
353     {
354       const char *src_str = gpg_strsource (src << GPG_ERR_SOURCE_SHIFT);
355       if (src_str && !strcasecmp (str, src_str))
356 	{
357 	  if (*have_source)
358 	    return 0;
359 
360 	  *have_source = 1;
361 	  *err |= src << GPG_ERR_SOURCE_SHIFT;
362 	  return 1;
363 	}
364     }
365 
366   for (code = 0; code < GPG_ERR_CODE_DIM; code++)
367     {
368       const char *code_str = gpg_strerror (code);
369       if (code_str && !strcasecmp (str, code_str))
370 	{
371 	  if (*have_code)
372 	    return 0;
373 
374 	  *have_code = 1;
375 	  *err |= code;
376 	  return 1;
377 	}
378     }
379 
380   return 0;
381 }
382 
383 
384 /* Parse string STR assuming it is either a single desription string C
385  * or in the form S.C to denote an error source descrition S and and
386  * error code description C.  Returns false on error (e.g. invalid
387  * symbol) or true for valid codes; if true is returned a full error
388  * code is stored at ERR.  */
389 static int
get_err_from_str(char * str,gpg_error_t * err)390 get_err_from_str (char *str, gpg_error_t *err)
391 {
392   char *str2 = str;
393   int have_source = 0;
394   int have_code = 0;
395   int ret;
396   char *saved_pos = NULL;
397   char saved_char = 0; /* (avoid warning) */
398 
399   *err = 0;
400   /* First match on the entire string to handle the case that it is
401    * code description with spaces.  */
402   ret = get_err_from_str_one (str, err, &have_source, &have_code);
403   if (ret)
404     return ret;
405 
406   /* Then figure out whether the first string is a simple word.  */
407   while (*str2 && ((*str2 >= 'A' && *str2 <= 'Z')
408 		   || (*str2 >= 'a' && *str2 <= 'z')
409 		   || (*str2 >= '0' && *str2 <= '9')
410 		   || *str2 == '_'))
411     str2++;
412   if (*str2)
413     {
414       saved_pos = str2;
415       saved_char = *str2;
416       *((char *) str2) = '\0';
417       str2++;
418       while (*str2 && !((*str2 >= 'A' && *str2 <= 'Z')
419 			|| (*str2 >= 'a' && *str2 <= 'z')
420 			|| (*str2 >= '0' && *str2 <= '9')
421 			|| *str2 == '_'))
422 	str2++;
423     }
424   else
425     str2 = NULL;
426 
427   ret = get_err_from_str_one (str, err, &have_source, &have_code);
428   if (ret && str2)
429     ret = get_err_from_str_one (str2, err, &have_source, &have_code);
430 
431   if (saved_pos)
432     *saved_pos = saved_char;
433   return ret;
434 }
435 
436 
437 static void
print_desc(const char * symbol)438 print_desc (const char *symbol)
439 {
440   static int initialized;
441   static FILE *fp;
442   char line[512];
443   char *p;
444   int indesc = 0;
445   int blanklines = 0;
446   int last_was_keyword = 0;
447 
448   if (!symbol)
449     return;
450 
451   if (!initialized)
452     {
453       initialized = 1;
454       fp = fopen (PKGDATADIR "/errorref.txt", "r");
455     }
456   if (!fp)
457     return;
458   rewind (fp);
459   while (fgets (line, sizeof line, fp))
460     {
461       if (*line == '#')
462         continue;
463       if (*line && line[strlen(line)-1] == '\n')
464         line[strlen(line)-1] = 0;
465 
466       if (!strncmp (line, "GPG_ERR_", 8))
467         {
468           if (indesc == 1 && last_was_keyword)
469             continue; /* Skip keywords immediately following a matched
470                        * keyword.  */
471           last_was_keyword = 1;
472 
473           indesc = 0;
474           p = strchr (line, ' ');
475           if (!p)
476             continue;
477           *p = 0;
478           if (!strcmp (line, symbol))
479             {
480               indesc = 1;
481               continue; /* Skip this line.  */
482             }
483         }
484       else
485         last_was_keyword = 0;
486       if (!indesc)
487         continue;
488       if (indesc == 1 && !*line)
489         continue; /* Skip leading empty lines in a description.  */
490       if (indesc == 1)
491         putchar ('\n'); /* One leading empty line.  */
492       indesc = 2;
493       if (!*line)
494         {
495           blanklines++;
496           continue;
497         }
498       for (; blanklines; blanklines--)
499         putchar ('\n');
500       printf ("%s\n", line);
501     }
502   putchar ('\n'); /* One trailing blank line.  */
503 }
504 
505 
506 
507 
508 
509 static const char *
my_strusage(int level)510 my_strusage (int level)
511 {
512   const char *p;
513 
514   switch (level)
515     {
516     case  9: p = "LGPL-2.1-or-later"; break;
517 
518     case 11: p = "gpg-error"; break;
519     case 12: p = PACKAGE_NAME; break;
520     case 13: p = PACKAGE_VERSION; break;
521     case 14: p = "Copyright (C) 2019 g10 Code GmbH"; break;
522     case 19: p = _("Please report bugs to <https://bugs.gnupg.org>.\n"); break;
523 
524     case 1:
525     case 40:
526       p = ("Usage: gpg-error [options] error-numbers");
527       break;
528     case 41:
529       p = ("Map error numbers to strings and vice versa.\n");
530       break;
531 
532     case 42:
533       p = "1"; /* Flag: print 40 as part of 41. */
534       break;
535 
536     default: p = NULL; break;
537     }
538   return p;
539 }
540 
541 
542 
543 int
main(int argc,char * argv[])544 main (int argc, char *argv[])
545 {
546   enum { CMD_DEFAULT     = 0,
547          CMD_LIB_VERSION = 501,
548          CMD_LIST,
549          CMD_DEFINES,
550          CMD_LOCALE,
551          OPT_DESC
552   };
553   static gpgrt_opt_t opts[] = {
554     ARGPARSE_c (CMD_LIB_VERSION, "lib-version",
555                 "Print library version"),
556     ARGPARSE_c (CMD_LIST, "list",
557                 "Print all error codes"),
558     ARGPARSE_c (CMD_DEFINES, "defines",
559                 "Print all error codes as #define lines"),
560 #if HAVE_W32_SYSTEM
561     ARGPARSE_c (CMD_LOCALE, "locale",
562                 "Return the locale used for gettext"),
563 #else
564     ARGPARSE_c (CMD_LOCALE, "locale",
565                 "@"),
566 #endif
567     ARGPARSE_s_n (OPT_DESC, "desc",
568                   "Print with error description"),
569     ARGPARSE_end()
570   };
571   gpgrt_argparse_t pargs = { &argc, &argv };
572 
573   int i;
574   int libversion = 0;
575   int listmode = 0;
576   int localemode = 0;
577   int desc = 0;
578   const char *s, *s2;
579   const char *source_sym;
580   const char *error_sym;
581   gpg_error_t err;
582 
583   gpgrt_init ();
584   i18n_init ();
585   gpgrt_set_strusage (my_strusage);
586   gpgrt_log_set_prefix (gpgrt_strusage (11), GPGRT_LOG_WITH_PREFIX);
587 
588 
589   while (gpgrt_argparse (NULL, &pargs, opts))
590     {
591       switch (pargs.r_opt)
592         {
593         case CMD_LIB_VERSION: libversion = 1; break;
594         case CMD_LIST:       listmode = 1; break;
595         case CMD_DEFINES:    listmode = 2; break;
596         case CMD_LOCALE:     localemode = 1; break;
597         case OPT_DESC:       desc = 1; break;
598         default: pargs.err = ARGPARSE_PRINT_WARNING; break;
599         }
600     }
601   gpgrt_argparse (NULL, &pargs, NULL);  /* Free internal memory.  */
602 
603   if (libversion)
604     {
605       if (argc)
606         gpgrt_usage (1);
607     }
608   else if (localemode)
609     {
610       if (argc > 1)
611         gpgrt_usage (1);
612     }
613   else if ((argc && listmode) || (!argc && !listmode))
614     gpgrt_usage (1);
615 
616 
617   if (libversion)
618     {
619       argc--; argv++;
620       printf ("Version from header: %s (0x%06x)\n",
621               GPG_ERROR_VERSION, GPG_ERROR_VERSION_NUMBER);
622       printf ("Version from binary: %s\n", gpg_error_check_version (NULL));
623       s = gpg_error_check_version ("\x01\x01");
624       while (*s && *s == '\n')
625         s++;
626       fputs ("Copyright blurb ...: ", stdout);
627       for (; *s; s++)
628         {
629           if (*s == '\n')
630             {
631               for (s2=s+1; *s2 == '\n'; s2++)
632                 ;
633               if (!*s2)
634                 break;  /* Cut off trailing LFs.  */
635               fputs ("\n                     ", stdout);
636             }
637           else
638             putc (*s, stdout);
639         }
640       putc ('\n', stdout);
641     }
642   else if (localemode)
643     {
644 #if HAVE_W32_SYSTEM
645       if (argc)
646         {
647           /* Warning: What we do here is not allowed because
648            * gpgrt_w32_override_locale needs to be called as early as
649            * possible.  However for this very purpose it is okay.  */
650           if (**argv >= '0' && **argv <= '9')
651             gpgrt_w32_override_locale (NULL, strtoul (*argv, NULL, 0));
652           else
653             gpgrt_w32_override_locale (*argv, 0);
654         }
655 
656       printf ("%s\n", gettext_localename ());
657 #else
658       log_info ("this command is only useful on Windows\n");
659 #endif
660     }
661   else if (listmode == 1)
662     {
663       for (i=0; i <  GPG_ERR_SOURCE_DIM; i++)
664         {
665           /* We use error code 1 because gpg_err_make requires a
666              non-zero error code. */
667           err = gpg_err_make (i, 1);
668           err -= 1;
669 	  source_sym = gpg_strsource_sym (err);
670           if (source_sym)
671             {
672               printf ("%u = (%u, -) = (%s, -) = (%s, -)\n",
673                       err, gpg_err_source (err),
674                       source_sym, gpg_strsource (err));
675               if (desc)
676                 print_desc (source_sym);
677             }
678         }
679       for (i=0; i <  GPG_ERR_CODE_DIM; i++)
680         {
681           err = gpg_err_make (GPG_ERR_SOURCE_UNKNOWN, i);
682 	  error_sym = gpg_strerror_sym (err);
683           if (error_sym)
684             {
685               printf ("%u = (-, %u) = (-, %s) = (-, %s)\n",
686                       err, gpg_err_code (err),
687                       error_sym, gpg_strerror (err));
688               if (desc)
689                 print_desc (error_sym);
690             }
691         }
692     }
693   else if (listmode == 2)
694     {
695       int n, nmax;
696 
697       for (i=0, nmax=0; i <  GPG_ERR_SOURCE_DIM; i++)
698         {
699           err = gpg_err_make (i, 1);
700 	  source_sym = gpg_strsource_sym (err);
701           if (source_sym)
702             {
703               n = strlen (source_sym);
704               if (n > nmax)
705                 nmax = n;
706             }
707         }
708       for (i=0; i <  GPG_ERR_SOURCE_DIM; i++)
709         {
710           err = gpg_err_make (i, 1);
711 	  source_sym = gpg_strsource_sym (err);
712           if (source_sym)
713             printf ("#define %-*s %3u\n", nmax,source_sym,gpg_err_source (err));
714         }
715 
716 
717       for (i=0, nmax = 0; i <  GPG_ERR_CODE_DIM; i++)
718         {
719           err = gpg_err_make (GPG_ERR_SOURCE_UNKNOWN, i);
720 	  error_sym = gpg_strerror_sym (err);
721           if (error_sym)
722             {
723               n = strlen (error_sym);
724               if (n > nmax)
725                 nmax = n;
726             }
727         }
728       for (i=0; i <  GPG_ERR_CODE_DIM; i++)
729         {
730           err = gpg_err_make (GPG_ERR_SOURCE_UNKNOWN, i);
731 	  error_sym = gpg_strerror_sym (err);
732           if (error_sym)
733             printf ("#define %-*s %5u\n", nmax, error_sym, gpg_err_code (err));
734         }
735     }
736   else /* Standard mode.  */
737     {
738       for (i=0; i < argc; i++)
739         {
740           /* First check the arg is a number N or K.N,
741            * then check the arg for CODESYM or SOURCESYM.CODESYM,
742            * then check the arg for CODESYM or CODESYM w/o GPG_ERR_ prefix,
743            * then check the arg for code description
744            *                     or symbol dot code description.
745            */
746           if (get_err_from_number (argv[i], &err)
747               || get_err_from_symbol (argv[i], &err)
748               || get_err_from_codesymbol (argv[i], &err)
749               || get_err_from_str (argv[i], &err))
750             {
751               source_sym = gpg_strsource_sym (err);
752               error_sym = gpg_strerror_sym (err);
753 
754               printf ("%u = (%u, %u) = (%s, %s) = (%s, %s)\n",
755                       err, gpg_err_source (err), gpg_err_code (err),
756                       source_sym ? source_sym : "-", error_sym ? error_sym:"-",
757                       gpg_strsource (err), gpg_strerror (err));
758               if (desc)
759                 print_desc (error_sym);
760             }
761           else
762             log_error (_("warning: could not recognize %s\n"), argv[i]);
763         }
764     }
765 
766   exit (0);
767 }
768