First take on changes to allow building with MSC for W32CE.
[gpgme.git] / src / engine-g13.c
1 /* engine-g13.c - G13 engine.
2    Copyright (C) 2000 Werner Koch (dd9jn)
3    Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, 2009 g10 Code GmbH
4  
5    This file is part of GPGME.
6
7    GPGME is free software; you can redistribute it and/or modify it
8    under the terms of the GNU Lesser General Public License as
9    published by the Free Software Foundation; either version 2.1 of
10    the License, or (at your option) any later version.
11    
12    GPGME is distributed in the hope that it will be useful, but
13    WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15    Lesser General Public License for more details.
16    
17    You should have received a copy of the GNU Lesser General Public
18    License along with this program; if not, write to the Free Software
19    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
20    02111-1307, USA.  */
21
22 #if HAVE_CONFIG_H
23 #include <config.h>
24 #endif
25
26 #include <stdlib.h>
27 #include <string.h>
28 #include <sys/types.h>
29 #include <assert.h>
30 #ifdef HAVE_UNISTD_H
31 # include <unistd.h>
32 #endif
33 #ifdef HAVE_LOCALE_H
34 #include <locale.h>
35 #endif
36 #include <fcntl.h> /* FIXME */
37 #include <errno.h>
38
39 #include "gpgme.h"
40 #include "util.h"
41 #include "ops.h"
42 #include "wait.h"
43 #include "priv-io.h"
44 #include "sema.h"
45
46 #include "assuan.h"
47 #include "debug.h"
48
49 #include "engine-backend.h"
50
51 \f
52 typedef struct
53 {
54   int fd;       /* FD we talk about.  */
55   int server_fd;/* Server FD for this connection.  */
56   int dir;      /* Inbound/Outbound, maybe given implicit?  */
57   void *data;   /* Handler-specific data.  */
58   void *tag;    /* ID from the user for gpgme_remove_io_callback.  */
59   char server_fd_str[15]; /* Same as SERVER_FD but as a string.  We
60                              need this because _gpgme_io_fd2str can't
61                              be used on a closed descriptor.  */
62 } iocb_data_t;
63
64
65 struct engine_g13
66 {
67   assuan_context_t assuan_ctx;
68
69   int lc_ctype_set;
70   int lc_messages_set;
71
72   iocb_data_t status_cb;
73
74   struct gpgme_io_cbs io_cbs;
75
76   /* User provided callbacks.  */
77   struct {
78     gpgme_assuan_data_cb_t data_cb;
79     void *data_cb_value;
80
81     gpgme_assuan_inquire_cb_t inq_cb;
82     void *inq_cb_value;
83
84     gpgme_assuan_status_cb_t status_cb;
85     void *status_cb_value;
86   } user;
87 };
88
89 typedef struct engine_g13 *engine_g13_t;
90
91
92 static void g13_io_event (void *engine, 
93                             gpgme_event_io_t type, void *type_data);
94
95
96 \f
97 static char *
98 g13_get_version (const char *file_name)
99 {
100   return _gpgme_get_program_version (file_name ? file_name
101                                      : _gpgme_get_g13_path ());
102 }
103
104
105 static const char *
106 g13_get_req_version (void)
107 {
108   return NEED_G13_VERSION;
109 }
110
111 \f
112 static void
113 close_notify_handler (int fd, void *opaque)
114 {
115   engine_g13_t g13 = opaque;
116
117   assert (fd != -1);
118   if (g13->status_cb.fd == fd)
119     {
120       if (g13->status_cb.tag)
121         (*g13->io_cbs.remove) (g13->status_cb.tag);
122       g13->status_cb.fd = -1;
123       g13->status_cb.tag = NULL;
124     }
125 }
126
127
128 /* This is the default inquiry callback.  We use it to handle the
129    Pinentry notifications.  */
130 static gpgme_error_t
131 default_inq_cb (engine_g13_t g13, const char *keyword, const char *args)
132 {
133   gpg_error_t err;
134
135   if (!strcmp (keyword, "PINENTRY_LAUNCHED"))
136     {
137       _gpgme_allow_set_foreground_window ((pid_t)strtoul (args, NULL, 10));
138     }
139
140   if (g13->user.inq_cb)
141     {
142       gpgme_data_t data = NULL;
143
144       err = g13->user.inq_cb (g13->user.inq_cb_value,
145                                 keyword, args, &data);
146       if (!err && data)
147         {
148           /* FIXME: Returning data is not yet implemented.  However we
149              need to allow the caller to cleanup his data object.
150              Thus we run the callback in finish mode immediately.  */
151           err = g13->user.inq_cb (g13->user.inq_cb_value,
152                                   NULL, NULL, &data);
153         }
154     }
155   else
156     err = 0;
157
158   return err;
159 }
160
161
162 static gpgme_error_t
163 g13_cancel (void *engine)
164 {
165   engine_g13_t g13 = engine;
166
167   if (!g13)
168     return gpg_error (GPG_ERR_INV_VALUE);
169
170   if (g13->status_cb.fd != -1)
171     _gpgme_io_close (g13->status_cb.fd);
172
173   if (g13->assuan_ctx)
174     {
175       assuan_release (g13->assuan_ctx);
176       g13->assuan_ctx = NULL;
177     }
178
179   return 0;
180 }
181
182
183 static gpgme_error_t
184 g13_cancel_op (void *engine)
185 {
186   engine_g13_t g13 = engine;
187
188   if (!g13)
189     return gpg_error (GPG_ERR_INV_VALUE);
190
191   if (g13->status_cb.fd != -1)
192     _gpgme_io_close (g13->status_cb.fd);
193
194   return 0;
195 }
196
197
198 static void
199 g13_release (void *engine)
200 {
201   engine_g13_t g13 = engine;
202
203   if (!g13)
204     return;
205
206   g13_cancel (engine);
207
208   free (g13);
209 }
210
211
212 static gpgme_error_t
213 g13_new (void **engine, const char *file_name, const char *home_dir)
214 {
215   gpgme_error_t err = 0;
216   engine_g13_t g13;
217   int argc;
218   const char *argv[5];
219   char *dft_display = NULL;
220   char dft_ttyname[64];
221   char *dft_ttytype = NULL;
222   char *optstr;
223
224   g13 = calloc (1, sizeof *g13);
225   if (!g13)
226     return gpg_error_from_errno (errno);
227
228   g13->status_cb.fd = -1;
229   g13->status_cb.dir = 1;
230   g13->status_cb.tag = 0;
231   g13->status_cb.data = g13;
232
233   argc = 0;
234   argv[argc++] = "g13";
235   if (home_dir)
236     {
237       argv[argc++] = "--homedir";
238       argv[argc++] = home_dir;
239     }
240   argv[argc++] = "--server";
241   argv[argc++] = NULL;
242
243   err = assuan_new_ext (&g13->assuan_ctx, GPG_ERR_SOURCE_GPGME,
244                         &_gpgme_assuan_malloc_hooks, _gpgme_assuan_log_cb,
245                         NULL);
246   if (err)
247     goto leave;
248   assuan_ctx_set_system_hooks (g13->assuan_ctx, &_gpgme_assuan_system_hooks);
249
250 #if USE_DESCRIPTOR_PASSING
251   err = assuan_pipe_connect
252     (g13->assuan_ctx, file_name ? file_name : _gpgme_get_g13_path (),
253      argv, NULL, NULL, NULL, ASSUAN_PIPE_CONNECT_FDPASSING);
254 #else
255   err = assuan_pipe_connect
256     (g13->assuan_ctx, file_name ? file_name : _gpgme_get_g13_path (),
257      argv, NULL, NULL, NULL, 0);
258 #endif
259   if (err)
260     goto leave;
261
262   err = _gpgme_getenv ("DISPLAY", &dft_display);
263   if (err)
264     goto leave;
265   if (dft_display)
266     {
267       if (asprintf (&optstr, "OPTION display=%s", dft_display) < 0)
268         {
269           free (dft_display);
270           err = gpg_error_from_errno (errno);
271           goto leave;
272         }
273       free (dft_display);
274
275       err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL, NULL,
276                              NULL, NULL, NULL);
277       free (optstr);
278       if (err)
279         goto leave;
280     }
281
282   if (isatty (1))
283     {
284       int rc;
285
286       rc = ttyname_r (1, dft_ttyname, sizeof (dft_ttyname));
287       if (rc)
288         {
289           err = gpg_error_from_errno (rc);
290           goto leave;
291         }
292       else
293         {
294           if (asprintf (&optstr, "OPTION ttyname=%s", dft_ttyname) < 0)
295             {
296               err = gpg_error_from_errno (errno);
297               goto leave;
298             }
299           err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL, NULL,
300                                  NULL, NULL, NULL);
301           free (optstr);
302           if (err)
303             goto leave;
304
305           err = _gpgme_getenv ("TERM", &dft_ttytype);
306           if (err)
307             goto leave;
308           if (dft_ttytype)
309             {
310               if (asprintf (&optstr, "OPTION ttytype=%s", dft_ttytype) < 0)
311                 {
312                   free (dft_ttytype);
313                   err = gpg_error_from_errno (errno);
314                   goto leave;
315                 }
316               free (dft_ttytype);
317
318               err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL,
319                                      NULL, NULL, NULL, NULL);
320               free (optstr);
321               if (err)
322                 goto leave;
323             }
324         }
325     }
326
327 #ifdef HAVE_W32_SYSTEM
328   /* Under Windows we need to use AllowSetForegroundWindow.  Tell
329      g13 to tell us when it needs it.  */
330   if (!err)
331     {
332       err = assuan_transact (g13->assuan_ctx, "OPTION allow-pinentry-notify",
333                              NULL, NULL, NULL, NULL, NULL, NULL);
334       if (gpg_err_code (err) == GPG_ERR_UNKNOWN_OPTION)
335         err = 0; /* This is a new feature of g13.  */
336     }
337 #endif /*HAVE_W32_SYSTEM*/
338
339  leave:
340
341   if (err)
342     g13_release (g13);
343   else
344     *engine = g13;
345
346   return err;
347 }
348
349
350 static gpgme_error_t
351 g13_set_locale (void *engine, int category, const char *value)
352 {
353   engine_g13_t g13 = engine;
354   gpgme_error_t err;
355   char *optstr;
356   char *catstr;
357
358   /* FIXME: If value is NULL, we need to reset the option to default.
359      But we can't do this.  So we error out here.  G13 needs support
360      for this.  */
361   if (0)
362     ;
363 #ifdef LC_CTYPE
364   else if (category == LC_CTYPE)
365     {
366       catstr = "lc-ctype";
367       if (!value && g13->lc_ctype_set)
368         return gpg_error (GPG_ERR_INV_VALUE);
369       if (value)
370         g13->lc_ctype_set = 1;
371     }
372 #endif
373 #ifdef LC_MESSAGES
374   else if (category == LC_MESSAGES)
375     {
376       catstr = "lc-messages";
377       if (!value && g13->lc_messages_set)
378         return gpg_error (GPG_ERR_INV_VALUE);
379       if (value)
380         g13->lc_messages_set = 1;
381     }
382 #endif /* LC_MESSAGES */
383   else
384     return gpg_error (GPG_ERR_INV_VALUE);
385
386   /* FIXME: Reset value to default.  */
387   if (!value) 
388     return 0;
389
390   if (asprintf (&optstr, "OPTION %s=%s", catstr, value) < 0)
391     err = gpg_error_from_errno (errno);
392   else
393     {
394       err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL,
395                              NULL, NULL, NULL, NULL);
396       free (optstr);
397     }
398
399   return err;
400 }
401
402
403 #if USE_DESCRIPTOR_PASSING
404 static gpgme_error_t
405 g13_assuan_simple_command (assuan_context_t ctx, char *cmd,
406                            engine_status_handler_t status_fnc,
407                            void *status_fnc_value)
408 {
409   gpg_error_t err;
410   char *line;
411   size_t linelen;
412
413   err = assuan_write_line (ctx, cmd);
414   if (err)
415     return err;
416
417   do
418     {
419       err = assuan_read_line (ctx, &line, &linelen);
420       if (err)
421         return err;
422
423       if (*line == '#' || !linelen)
424         continue;
425
426       if (linelen >= 2
427           && line[0] == 'O' && line[1] == 'K'
428           && (line[2] == '\0' || line[2] == ' '))
429         return 0;
430       else if (linelen >= 4
431           && line[0] == 'E' && line[1] == 'R' && line[2] == 'R'
432           && line[3] == ' ')
433         err = atoi (&line[4]);
434       else if (linelen >= 2
435                && line[0] == 'S' && line[1] == ' ')
436         {
437           char *rest;
438
439           rest = strchr (line + 2, ' ');
440           if (!rest)
441             rest = line + linelen; /* set to an empty string */
442           else
443             *(rest++) = 0;
444
445           /* Nothing to do with status lines.  */
446         }
447       else
448         err = gpg_error (GPG_ERR_GENERAL);
449     }
450   while (!err);
451
452   return err;
453 }
454 #endif
455
456
457 static gpgme_error_t
458 status_handler (void *opaque, int fd)
459 {
460   struct io_cb_data *data = (struct io_cb_data *) opaque;
461   engine_g13_t g13 = (engine_g13_t) data->handler_value;
462   gpgme_error_t err = 0;
463   char *line;
464   size_t linelen;
465
466   do
467     {
468       err = assuan_read_line (g13->assuan_ctx, &line, &linelen);
469       if (err)
470         {
471           /* Try our best to terminate the connection friendly.  */
472           /*      assuan_write_line (g13->assuan_ctx, "BYE"); */
473           TRACE2 (DEBUG_CTX, "gpgme:status_handler", g13,
474                   "fd 0x%x: error reading assuan line: %s",
475                   fd, gpg_strerror (err));
476         }
477       else if (linelen >= 3
478                && line[0] == 'E' && line[1] == 'R' && line[2] == 'R'
479                && (line[3] == '\0' || line[3] == ' '))
480         {
481           if (line[3] == ' ')
482             err = atoi (&line[4]);
483           if (! err)
484             err = gpg_error (GPG_ERR_GENERAL);
485           TRACE2 (DEBUG_CTX, "gpgme:status_handler", g13,
486                   "fd 0x%x: ERR line: %s",
487                   fd, err ? gpg_strerror (err) : "ok");
488           
489           /* Command execution errors are not fatal, as we use
490              a session based protocol.  */
491           data->op_err = err;
492
493           /* The caller will do the rest (namely, call cancel_op,
494              which closes status_fd).  */
495           return 0;
496         }
497       else if (linelen >= 2
498                && line[0] == 'O' && line[1] == 'K'
499                && (line[2] == '\0' || line[2] == ' '))
500         {
501           TRACE1 (DEBUG_CTX, "gpgme:status_handler", g13,
502                   "fd 0x%x: OK line", fd);
503
504           _gpgme_io_close (g13->status_cb.fd);
505           return 0;
506         }
507       else if (linelen > 2
508                && line[0] == 'D' && line[1] == ' ')
509         {
510           /* We are using the colon handler even for plain inline data
511              - strange name for that function but for historic reasons
512              we keep it.  */
513           /* FIXME We can't use this for binary data because we
514              assume this is a string.  For the current usage of colon
515              output it is correct.  */
516           char *src = line + 2;
517           char *end = line + linelen;
518           char *dst = src;
519
520           linelen = 0;
521           while (src < end)
522             {
523               if (*src == '%' && src + 2 < end)
524                 {
525                   /* Handle escaped characters.  */
526                   ++src;
527                   *dst++ = _gpgme_hextobyte (src);
528                   src += 2;
529                 }
530               else
531                 *dst++ = *src++;
532
533               linelen++;
534             }
535
536           src = line + 2;
537           if (linelen && g13->user.data_cb)
538             err = g13->user.data_cb (g13->user.data_cb_value,
539                                        src, linelen);
540           else
541             err = 0;
542
543           TRACE2 (DEBUG_CTX, "gpgme:g13_status_handler", g13,
544                   "fd 0x%x: D inlinedata; status from cb: %s",
545                   fd, (g13->user.data_cb ?
546                        (err? gpg_strerror (err):"ok"):"no callback"));
547
548         }
549       else if (linelen > 2
550                && line[0] == 'S' && line[1] == ' ')
551         {
552           char *src;
553           char *args;
554
555           src = line + 2;
556           while (*src == ' ')
557             src++;
558           
559           args = strchr (line + 2, ' ');
560           if (!args)
561             args = line + linelen; /* set to an empty string */
562           else
563             *(args++) = 0;
564
565           while (*args == ' ')
566             args++;
567
568           if (g13->user.status_cb)
569             err = g13->user.status_cb (g13->user.status_cb_value,
570                                        src, args);
571           else
572             err = 0;
573
574           TRACE3 (DEBUG_CTX, "gpgme:g13_status_handler", g13,
575                   "fd 0x%x: S line (%s) - status from cb: %s",
576                   fd, line+2, (g13->user.status_cb ?
577                                (err? gpg_strerror (err):"ok"):"no callback"));
578         }
579       else if (linelen >= 7
580                && line[0] == 'I' && line[1] == 'N' && line[2] == 'Q'
581                && line[3] == 'U' && line[4] == 'I' && line[5] == 'R'
582                && line[6] == 'E' 
583                && (line[7] == '\0' || line[7] == ' '))
584         {
585           char *src;
586           char *args;
587           
588           for (src=line+7; *src == ' '; src++)
589             ;
590
591           args = strchr (src, ' ');
592           if (!args)
593             args = line + linelen; /* Let it point to an empty string.  */
594           else
595             *(args++) = 0;
596
597           while (*args == ' ')
598             args++;
599
600           err = default_inq_cb (g13, src, args);
601           if (!err) 
602             {
603               /* Flush and send END.  */
604               err = assuan_send_data (g13->assuan_ctx, NULL, 0);
605             }
606           else if (gpg_err_code (err) == GPG_ERR_ASS_CANCELED)
607             {
608               /* Flush and send CANcel.  */
609               err = assuan_send_data (g13->assuan_ctx, NULL, 1);
610             }
611           assuan_write_line (g13->assuan_ctx, "END");
612         }
613     }
614   while (!err && assuan_pending_line (g13->assuan_ctx));
615   
616   return err;
617 }
618   
619
620 static gpgme_error_t
621 add_io_cb (engine_g13_t g13, iocb_data_t *iocbd, gpgme_io_cb_t handler)
622 {
623   gpgme_error_t err;
624
625   TRACE_BEG2 (DEBUG_ENGINE, "engine-g13:add_io_cb", g13,
626               "fd %d, dir %d", iocbd->fd, iocbd->dir);
627   err = (*g13->io_cbs.add) (g13->io_cbs.add_priv,
628                               iocbd->fd, iocbd->dir,
629                               handler, iocbd->data, &iocbd->tag);
630   if (err)
631     return TRACE_ERR (err);
632   if (!iocbd->dir)
633     /* FIXME Kludge around poll() problem.  */
634     err = _gpgme_io_set_nonblocking (iocbd->fd);
635   return TRACE_ERR (err);
636 }
637
638
639 static gpgme_error_t
640 start (engine_g13_t g13, const char *command)
641 {
642   gpgme_error_t err;
643   assuan_fd_t afdlist[5];
644   int fdlist[5];
645   int nfds;
646   int i;
647
648   /* We need to know the fd used by assuan for reads.  We do this by
649      using the assumption that the first returned fd from
650      assuan_get_active_fds() is always this one.  */
651   nfds = assuan_get_active_fds (g13->assuan_ctx, 0 /* read fds */,
652                                 afdlist, DIM (afdlist));
653   if (nfds < 1)
654     return gpg_error (GPG_ERR_GENERAL); /* FIXME */
655   /* For now... */
656   for (i = 0; i < nfds; i++)
657     fdlist[i] = (int) afdlist[i];
658
659   /* We "duplicate" the file descriptor, so we can close it here (we
660      can't close fdlist[0], as that is closed by libassuan, and
661      closing it here might cause libassuan to close some unrelated FD
662      later).  Alternatively, we could special case status_fd and
663      register/unregister it manually as needed, but this increases
664      code duplication and is more complicated as we can not use the
665      close notifications etc.  A third alternative would be to let
666      Assuan know that we closed the FD, but that complicates the
667      Assuan interface.  */
668
669   g13->status_cb.fd = _gpgme_io_dup (fdlist[0]);
670   if (g13->status_cb.fd < 0)
671     return gpg_error_from_syserror ();
672
673   if (_gpgme_io_set_close_notify (g13->status_cb.fd,
674                                   close_notify_handler, g13))
675     {
676       _gpgme_io_close (g13->status_cb.fd);
677       g13->status_cb.fd = -1;
678       return gpg_error (GPG_ERR_GENERAL);
679     }
680
681   err = add_io_cb (g13, &g13->status_cb, status_handler);
682   if (!err)
683     err = assuan_write_line (g13->assuan_ctx, command);
684
685   if (!err)
686     g13_io_event (g13, GPGME_EVENT_START, NULL);
687
688   return err;
689 }
690
691
692 #if USE_DESCRIPTOR_PASSING
693 static gpgme_error_t
694 g13_reset (void *engine)
695 {
696   engine_g13_t g13 = engine;
697
698   /* We must send a reset because we need to reset the list of
699      signers.  Note that RESET does not reset OPTION commands. */
700   return g13_assuan_simple_command (g13->assuan_ctx, "RESET", NULL, NULL);
701 }
702 #endif
703
704
705 static gpgme_error_t
706 g13_transact (void *engine,
707                 const char *command,
708                 gpgme_assuan_data_cb_t data_cb,
709                 void *data_cb_value,
710                 gpgme_assuan_inquire_cb_t inq_cb,
711                 void *inq_cb_value,
712                 gpgme_assuan_status_cb_t status_cb,
713                 void *status_cb_value)
714 {
715   engine_g13_t g13 = engine;
716   gpgme_error_t err;
717
718   if (!g13 || !command || !*command)
719     return gpg_error (GPG_ERR_INV_VALUE);
720
721   g13->user.data_cb = data_cb;
722   g13->user.data_cb_value = data_cb_value;
723   g13->user.inq_cb = inq_cb;
724   g13->user.inq_cb_value = inq_cb_value;
725   g13->user.status_cb = status_cb;
726   g13->user.status_cb_value = status_cb_value;
727
728   err = start (g13, command);
729   return err;
730 }
731
732
733
734 static void
735 g13_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs)
736 {
737   engine_g13_t g13 = engine;
738   g13->io_cbs = *io_cbs;
739 }
740
741
742 static void
743 g13_io_event (void *engine, gpgme_event_io_t type, void *type_data)
744 {
745   engine_g13_t g13 = engine;
746
747   TRACE3 (DEBUG_ENGINE, "gpgme:g13_io_event", g13,
748           "event %p, type %d, type_data %p",
749           g13->io_cbs.event, type, type_data);
750   if (g13->io_cbs.event)
751     (*g13->io_cbs.event) (g13->io_cbs.event_priv, type, type_data);
752 }
753
754
755 struct engine_ops _gpgme_engine_ops_g13 =
756   {
757     /* Static functions.  */
758     _gpgme_get_g13_path,
759     NULL,
760     g13_get_version,
761     g13_get_req_version,
762     g13_new,
763
764     /* Member functions.  */
765     g13_release,
766 #if USE_DESCRIPTOR_PASSING
767     g13_reset,
768 #else
769     NULL,                       /* reset */
770 #endif
771     NULL,               /* set_status_handler */
772     NULL,               /* set_command_handler */
773     NULL,               /* set_colon_line_handler */
774     g13_set_locale,
775     NULL,               /* set_protocol */
776     NULL,               /* decrypt */
777     NULL,               /* decrypt_verify */
778     NULL,               /* delete */
779     NULL,               /* edit */
780     NULL,               /* encrypt */
781     NULL,               /* encrypt_sign */
782     NULL,               /* export */
783     NULL,               /* export_ext */
784     NULL,               /* genkey */
785     NULL,               /* import */
786     NULL,               /* keylist */
787     NULL,               /* keylist_ext */
788     NULL,               /* sign */
789     NULL,               /* trustlist */
790     NULL,               /* verify */
791     NULL,               /* getauditlog */
792     g13_transact,
793     NULL,               /* conf_load */
794     NULL,               /* conf_save */
795     g13_set_io_cbs,
796     g13_io_event,
797     g13_cancel,
798     g13_cancel_op,
799   };