1 /*
2   Simple DirectMedia Layer
3   Copyright (C) 1997-2016 Sam Lantinga <slouken@libsdl.org>
4 
5   This software is provided 'as-is', without any express or implied
6   warranty.  In no event will the authors be held liable for any damages
7   arising from the use of this software.
8 
9   Permission is granted to anyone to use this software for any purpose,
10   including commercial applications, and to alter it and redistribute it
11   freely, subject to the following restrictions:
12 
13   1. The origin of this software must not be misrepresented; you must not
14      claim that you wrote the original software. If you use this software
15      in a product, an acknowledgment in the product documentation would be
16      appreciated but is not required.
17   2. Altered source versions must be plainly marked as such, and must not be
18      misrepresented as being the original software.
19   3. This notice may not be removed or altered from any source distribution.
20 */
21 #include "../../SDL_internal.h"
22 
23 #ifdef HAVE_IBUS_IBUS_H
24 #include "SDL.h"
25 #include "SDL_syswm.h"
26 #include "SDL_ibus.h"
27 #include "SDL_dbus.h"
28 #include "../../video/SDL_sysvideo.h"
29 #include "../../events/SDL_keyboard_c.h"
30 
31 #if SDL_VIDEO_DRIVER_X11
32     #include "../../video/x11/SDL_x11video.h"
33 #endif
34 
35 #include <sys/inotify.h>
36 #include <unistd.h>
37 #include <fcntl.h>
38 
39 static const char IBUS_SERVICE[]         = "org.freedesktop.IBus";
40 static const char IBUS_PATH[]            = "/org/freedesktop/IBus";
41 static const char IBUS_INTERFACE[]       = "org.freedesktop.IBus";
42 static const char IBUS_INPUT_INTERFACE[] = "org.freedesktop.IBus.InputContext";
43 
44 static char *input_ctx_path = NULL;
45 static SDL_Rect ibus_cursor_rect = { 0, 0, 0, 0 };
46 static DBusConnection *ibus_conn = NULL;
47 static char *ibus_addr_file = NULL;
48 int inotify_fd = -1, inotify_wd = -1;
49 
50 static Uint32
IBus_ModState(void)51 IBus_ModState(void)
52 {
53     Uint32 ibus_mods = 0;
54     SDL_Keymod sdl_mods = SDL_GetModState();
55 
56     /* Not sure about MOD3, MOD4 and HYPER mappings */
57     if (sdl_mods & KMOD_LSHIFT) ibus_mods |= IBUS_SHIFT_MASK;
58     if (sdl_mods & KMOD_CAPS)   ibus_mods |= IBUS_LOCK_MASK;
59     if (sdl_mods & KMOD_LCTRL)  ibus_mods |= IBUS_CONTROL_MASK;
60     if (sdl_mods & KMOD_LALT)   ibus_mods |= IBUS_MOD1_MASK;
61     if (sdl_mods & KMOD_NUM)    ibus_mods |= IBUS_MOD2_MASK;
62     if (sdl_mods & KMOD_MODE)   ibus_mods |= IBUS_MOD5_MASK;
63     if (sdl_mods & KMOD_LGUI)   ibus_mods |= IBUS_SUPER_MASK;
64     if (sdl_mods & KMOD_RGUI)   ibus_mods |= IBUS_META_MASK;
65 
66     return ibus_mods;
67 }
68 
69 static const char *
IBus_GetVariantText(DBusConnection * conn,DBusMessageIter * iter,SDL_DBusContext * dbus)70 IBus_GetVariantText(DBusConnection *conn, DBusMessageIter *iter, SDL_DBusContext *dbus)
71 {
72     /* The text we need is nested weirdly, use dbus-monitor to see the structure better */
73     const char *text = NULL;
74     const char *struct_id = NULL;
75     DBusMessageIter sub1, sub2;
76 
77     if (dbus->message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) {
78         return NULL;
79     }
80 
81     dbus->message_iter_recurse(iter, &sub1);
82 
83     if (dbus->message_iter_get_arg_type(&sub1) != DBUS_TYPE_STRUCT) {
84         return NULL;
85     }
86 
87     dbus->message_iter_recurse(&sub1, &sub2);
88 
89     if (dbus->message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) {
90         return NULL;
91     }
92 
93     dbus->message_iter_get_basic(&sub2, &struct_id);
94     if (!struct_id || SDL_strncmp(struct_id, "IBusText", sizeof("IBusText")) != 0) {
95         return NULL;
96     }
97 
98     dbus->message_iter_next(&sub2);
99     dbus->message_iter_next(&sub2);
100 
101     if (dbus->message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) {
102         return NULL;
103     }
104 
105     dbus->message_iter_get_basic(&sub2, &text);
106 
107     return text;
108 }
109 
110 static size_t
IBus_utf8_strlen(const char * str)111 IBus_utf8_strlen(const char *str)
112 {
113     size_t utf8_len = 0;
114     const char *p;
115 
116     for (p = str; *p; ++p) {
117         if (!((*p & 0x80) && !(*p & 0x40))) {
118             ++utf8_len;
119         }
120     }
121 
122     return utf8_len;
123 }
124 
125 static DBusHandlerResult
IBus_MessageHandler(DBusConnection * conn,DBusMessage * msg,void * user_data)126 IBus_MessageHandler(DBusConnection *conn, DBusMessage *msg, void *user_data)
127 {
128     SDL_DBusContext *dbus = (SDL_DBusContext *)user_data;
129 
130     if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "CommitText")) {
131         DBusMessageIter iter;
132         const char *text;
133 
134         dbus->message_iter_init(msg, &iter);
135 
136         text = IBus_GetVariantText(conn, &iter, dbus);
137         if (text && *text) {
138             char buf[SDL_TEXTEDITINGEVENT_TEXT_SIZE];
139             size_t text_bytes = SDL_strlen(text), i = 0;
140 
141             while (i < text_bytes) {
142                 size_t sz = SDL_utf8strlcpy(buf, text+i, sizeof(buf));
143                 SDL_SendKeyboardText(buf);
144 
145                 i += sz;
146             }
147         }
148 
149         return DBUS_HANDLER_RESULT_HANDLED;
150     }
151 
152     if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "UpdatePreeditText")) {
153         DBusMessageIter iter;
154         const char *text;
155 
156         dbus->message_iter_init(msg, &iter);
157         text = IBus_GetVariantText(conn, &iter, dbus);
158 
159         if (text) {
160             char buf[SDL_TEXTEDITINGEVENT_TEXT_SIZE];
161             size_t text_bytes = SDL_strlen(text), i = 0;
162             size_t cursor = 0;
163 
164             do {
165                 size_t sz = SDL_utf8strlcpy(buf, text+i, sizeof(buf));
166                 size_t chars = IBus_utf8_strlen(buf);
167 
168                 SDL_SendEditingText(buf, cursor, chars);
169 
170                 i += sz;
171                 cursor += chars;
172             } while (i < text_bytes);
173         }
174 
175         SDL_IBus_UpdateTextRect(NULL);
176 
177         return DBUS_HANDLER_RESULT_HANDLED;
178     }
179 
180     if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "HidePreeditText")) {
181         SDL_SendEditingText("", 0, 0);
182         return DBUS_HANDLER_RESULT_HANDLED;
183     }
184 
185     return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
186 }
187 
188 static char *
IBus_ReadAddressFromFile(const char * file_path)189 IBus_ReadAddressFromFile(const char *file_path)
190 {
191     char addr_buf[1024];
192     SDL_bool success = SDL_FALSE;
193     FILE *addr_file;
194 
195     addr_file = fopen(file_path, "r");
196     if (!addr_file) {
197         return NULL;
198     }
199 
200     while (fgets(addr_buf, sizeof(addr_buf), addr_file)) {
201         if (SDL_strncmp(addr_buf, "IBUS_ADDRESS=", sizeof("IBUS_ADDRESS=")-1) == 0) {
202             size_t sz = SDL_strlen(addr_buf);
203             if (addr_buf[sz-1] == '\n') addr_buf[sz-1] = 0;
204             if (addr_buf[sz-2] == '\r') addr_buf[sz-2] = 0;
205             success = SDL_TRUE;
206             break;
207         }
208     }
209 
210     fclose(addr_file);
211 
212     if (success) {
213         return SDL_strdup(addr_buf + (sizeof("IBUS_ADDRESS=") - 1));
214     } else {
215         return NULL;
216     }
217 }
218 
219 static char *
IBus_GetDBusAddressFilename(void)220 IBus_GetDBusAddressFilename(void)
221 {
222     SDL_DBusContext *dbus;
223     const char *disp_env;
224     char config_dir[PATH_MAX];
225     char *display = NULL;
226     const char *addr;
227     const char *conf_env;
228     char *key;
229     char file_path[PATH_MAX];
230     const char *host;
231     char *disp_num, *screen_num;
232 
233     if (ibus_addr_file) {
234         return SDL_strdup(ibus_addr_file);
235     }
236 
237     dbus = SDL_DBus_GetContext();
238     if (!dbus) {
239         return NULL;
240     }
241 
242     /* Use this environment variable if it exists. */
243     addr = SDL_getenv("IBUS_ADDRESS");
244     if (addr && *addr) {
245         return SDL_strdup(addr);
246     }
247 
248     /* Otherwise, we have to get the hostname, display, machine id, config dir
249        and look up the address from a filepath using all those bits, eek. */
250     disp_env = SDL_getenv("DISPLAY");
251 
252     if (!disp_env || !*disp_env) {
253         display = SDL_strdup(":0.0");
254     } else {
255         display = SDL_strdup(disp_env);
256     }
257 
258     host = display;
259     disp_num   = SDL_strrchr(display, ':');
260     screen_num = SDL_strrchr(display, '.');
261 
262     if (!disp_num) {
263         SDL_free(display);
264         return NULL;
265     }
266 
267     *disp_num = 0;
268     disp_num++;
269 
270     if (screen_num) {
271         *screen_num = 0;
272     }
273 
274     if (!*host) {
275         host = "unix";
276     }
277 
278     SDL_memset(config_dir, 0, sizeof(config_dir));
279 
280     conf_env = SDL_getenv("XDG_CONFIG_HOME");
281     if (conf_env && *conf_env) {
282         SDL_strlcpy(config_dir, conf_env, sizeof(config_dir));
283     } else {
284         const char *home_env = SDL_getenv("HOME");
285         if (!home_env || !*home_env) {
286             SDL_free(display);
287             return NULL;
288         }
289         SDL_snprintf(config_dir, sizeof(config_dir), "%s/.config", home_env);
290     }
291 
292     key = dbus->get_local_machine_id();
293 
294     SDL_memset(file_path, 0, sizeof(file_path));
295     SDL_snprintf(file_path, sizeof(file_path), "%s/ibus/bus/%s-%s-%s",
296                                                config_dir, key, host, disp_num);
297     dbus->free(key);
298     SDL_free(display);
299 
300     return SDL_strdup(file_path);
301 }
302 
303 static SDL_bool IBus_CheckConnection(SDL_DBusContext *dbus);
304 
305 static void
IBus_SetCapabilities(void * data,const char * name,const char * old_val,const char * internal_editing)306 IBus_SetCapabilities(void *data, const char *name, const char *old_val,
307                                                    const char *internal_editing)
308 {
309     SDL_DBusContext *dbus = SDL_DBus_GetContext();
310 
311     if (IBus_CheckConnection(dbus)) {
312 
313         DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
314                                                          input_ctx_path,
315                                                          IBUS_INPUT_INTERFACE,
316                                                          "SetCapabilities");
317         if (msg) {
318             Uint32 caps = IBUS_CAP_FOCUS;
319             if (!(internal_editing && *internal_editing == '1')) {
320                 caps |= IBUS_CAP_PREEDIT_TEXT;
321             }
322 
323             dbus->message_append_args(msg,
324                                       DBUS_TYPE_UINT32, &caps,
325                                       DBUS_TYPE_INVALID);
326         }
327 
328         if (msg) {
329             if (dbus->connection_send(ibus_conn, msg, NULL)) {
330                 dbus->connection_flush(ibus_conn);
331             }
332             dbus->message_unref(msg);
333         }
334     }
335 }
336 
337 
338 static SDL_bool
IBus_SetupConnection(SDL_DBusContext * dbus,const char * addr)339 IBus_SetupConnection(SDL_DBusContext *dbus, const char* addr)
340 {
341     const char *path = NULL;
342     SDL_bool result = SDL_FALSE;
343     DBusMessage *msg;
344     DBusObjectPathVTable ibus_vtable;
345 
346     SDL_zero(ibus_vtable);
347     ibus_vtable.message_function = &IBus_MessageHandler;
348 
349     ibus_conn = dbus->connection_open_private(addr, NULL);
350 
351     if (!ibus_conn) {
352         return SDL_FALSE;
353     }
354 
355     dbus->connection_flush(ibus_conn);
356 
357     if (!dbus->bus_register(ibus_conn, NULL)) {
358         ibus_conn = NULL;
359         return SDL_FALSE;
360     }
361 
362     dbus->connection_flush(ibus_conn);
363 
364     msg = dbus->message_new_method_call(IBUS_SERVICE, IBUS_PATH, IBUS_INTERFACE, "CreateInputContext");
365     if (msg) {
366         const char *client_name = "SDL2_Application";
367         dbus->message_append_args(msg,
368                                   DBUS_TYPE_STRING, &client_name,
369                                   DBUS_TYPE_INVALID);
370     }
371 
372     if (msg) {
373         DBusMessage *reply;
374 
375         reply = dbus->connection_send_with_reply_and_block(ibus_conn, msg, 1000, NULL);
376         if (reply) {
377             if (dbus->message_get_args(reply, NULL,
378                                        DBUS_TYPE_OBJECT_PATH, &path,
379                                        DBUS_TYPE_INVALID)) {
380                 if (input_ctx_path) {
381                     SDL_free(input_ctx_path);
382                 }
383                 input_ctx_path = SDL_strdup(path);
384                 result = SDL_TRUE;
385             }
386             dbus->message_unref(reply);
387         }
388         dbus->message_unref(msg);
389     }
390 
391     if (result) {
392         SDL_AddHintCallback(SDL_HINT_IME_INTERNAL_EDITING, &IBus_SetCapabilities, NULL);
393 
394         dbus->bus_add_match(ibus_conn, "type='signal',interface='org.freedesktop.IBus.InputContext'", NULL);
395         dbus->connection_try_register_object_path(ibus_conn, input_ctx_path, &ibus_vtable, dbus, NULL);
396         dbus->connection_flush(ibus_conn);
397     }
398 
399     SDL_IBus_SetFocus(SDL_GetKeyboardFocus() != NULL);
400     SDL_IBus_UpdateTextRect(NULL);
401 
402     return result;
403 }
404 
405 static SDL_bool
IBus_CheckConnection(SDL_DBusContext * dbus)406 IBus_CheckConnection(SDL_DBusContext *dbus)
407 {
408     if (!dbus) return SDL_FALSE;
409 
410     if (ibus_conn && dbus->connection_get_is_connected(ibus_conn)) {
411         return SDL_TRUE;
412     }
413 
414     if (inotify_fd > 0 && inotify_wd > 0) {
415         char buf[1024];
416         ssize_t readsize = read(inotify_fd, buf, sizeof(buf));
417         if (readsize > 0) {
418 
419             char *p;
420             SDL_bool file_updated = SDL_FALSE;
421 
422             for (p = buf; p < buf + readsize; /**/) {
423                 struct inotify_event *event = (struct inotify_event*) p;
424                 if (event->len > 0) {
425                     char *addr_file_no_path = SDL_strrchr(ibus_addr_file, '/');
426                     if (!addr_file_no_path) return SDL_FALSE;
427 
428                     if (SDL_strcmp(addr_file_no_path + 1, event->name) == 0) {
429                         file_updated = SDL_TRUE;
430                         break;
431                     }
432                 }
433 
434                 p += sizeof(struct inotify_event) + event->len;
435             }
436 
437             if (file_updated) {
438                 char *addr = IBus_ReadAddressFromFile(ibus_addr_file);
439                 if (addr) {
440                     SDL_bool result = IBus_SetupConnection(dbus, addr);
441                     SDL_free(addr);
442                     return result;
443                 }
444             }
445         }
446     }
447 
448     return SDL_FALSE;
449 }
450 
451 SDL_bool
SDL_IBus_Init(void)452 SDL_IBus_Init(void)
453 {
454     SDL_bool result = SDL_FALSE;
455     SDL_DBusContext *dbus = SDL_DBus_GetContext();
456 
457     if (dbus) {
458         char *addr_file = IBus_GetDBusAddressFilename();
459         char *addr;
460         char *addr_file_dir;
461 
462         if (!addr_file) {
463             return SDL_FALSE;
464         }
465 
466         /* !!! FIXME: if ibus_addr_file != NULL, this will overwrite it and leak (twice!) */
467         ibus_addr_file = SDL_strdup(addr_file);
468 
469         addr = IBus_ReadAddressFromFile(addr_file);
470         if (!addr) {
471             SDL_free(addr_file);
472             return SDL_FALSE;
473         }
474 
475         if (inotify_fd < 0) {
476             inotify_fd = inotify_init();
477             fcntl(inotify_fd, F_SETFL, O_NONBLOCK);
478         }
479 
480         addr_file_dir = SDL_strrchr(addr_file, '/');
481         if (addr_file_dir) {
482             *addr_file_dir = 0;
483         }
484 
485         inotify_wd = inotify_add_watch(inotify_fd, addr_file, IN_CREATE | IN_MODIFY);
486         SDL_free(addr_file);
487 
488         if (addr) {
489             result = IBus_SetupConnection(dbus, addr);
490             SDL_free(addr);
491         }
492     }
493 
494     return result;
495 }
496 
497 void
SDL_IBus_Quit(void)498 SDL_IBus_Quit(void)
499 {
500     SDL_DBusContext *dbus;
501 
502     if (input_ctx_path) {
503         SDL_free(input_ctx_path);
504         input_ctx_path = NULL;
505     }
506 
507     if (ibus_addr_file) {
508         SDL_free(ibus_addr_file);
509         ibus_addr_file = NULL;
510     }
511 
512     dbus = SDL_DBus_GetContext();
513 
514     if (dbus && ibus_conn) {
515         dbus->connection_close(ibus_conn);
516         dbus->connection_unref(ibus_conn);
517     }
518 
519     if (inotify_fd > 0 && inotify_wd > 0) {
520         inotify_rm_watch(inotify_fd, inotify_wd);
521         inotify_wd = -1;
522     }
523 
524     SDL_DelHintCallback(SDL_HINT_IME_INTERNAL_EDITING, &IBus_SetCapabilities, NULL);
525 
526     SDL_memset(&ibus_cursor_rect, 0, sizeof(ibus_cursor_rect));
527 }
528 
529 static void
IBus_SimpleMessage(const char * method)530 IBus_SimpleMessage(const char *method)
531 {
532     SDL_DBusContext *dbus = SDL_DBus_GetContext();
533 
534     if (IBus_CheckConnection(dbus)) {
535         DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
536                                                          input_ctx_path,
537                                                          IBUS_INPUT_INTERFACE,
538                                                          method);
539         if (msg) {
540             if (dbus->connection_send(ibus_conn, msg, NULL)) {
541                 dbus->connection_flush(ibus_conn);
542             }
543             dbus->message_unref(msg);
544         }
545     }
546 }
547 
548 void
SDL_IBus_SetFocus(SDL_bool focused)549 SDL_IBus_SetFocus(SDL_bool focused)
550 {
551     const char *method = focused ? "FocusIn" : "FocusOut";
552     IBus_SimpleMessage(method);
553 }
554 
555 void
SDL_IBus_Reset(void)556 SDL_IBus_Reset(void)
557 {
558     IBus_SimpleMessage("Reset");
559 }
560 
561 SDL_bool
SDL_IBus_ProcessKeyEvent(Uint32 keysym,Uint32 keycode)562 SDL_IBus_ProcessKeyEvent(Uint32 keysym, Uint32 keycode)
563 {
564     SDL_bool result = SDL_FALSE;
565     SDL_DBusContext *dbus = SDL_DBus_GetContext();
566 
567     if (IBus_CheckConnection(dbus)) {
568         DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
569                                                          input_ctx_path,
570                                                          IBUS_INPUT_INTERFACE,
571                                                          "ProcessKeyEvent");
572         if (msg) {
573             Uint32 mods = IBus_ModState();
574             dbus->message_append_args(msg,
575                                       DBUS_TYPE_UINT32, &keysym,
576                                       DBUS_TYPE_UINT32, &keycode,
577                                       DBUS_TYPE_UINT32, &mods,
578                                       DBUS_TYPE_INVALID);
579         }
580 
581         if (msg) {
582             DBusMessage *reply;
583 
584             reply = dbus->connection_send_with_reply_and_block(ibus_conn, msg, 300, NULL);
585             if (reply) {
586                 if (!dbus->message_get_args(reply, NULL,
587                                            DBUS_TYPE_BOOLEAN, &result,
588                                            DBUS_TYPE_INVALID)) {
589                     result = SDL_FALSE;
590                 }
591                 dbus->message_unref(reply);
592             }
593             dbus->message_unref(msg);
594         }
595 
596     }
597 
598     SDL_IBus_UpdateTextRect(NULL);
599 
600     return result;
601 }
602 
603 void
SDL_IBus_UpdateTextRect(SDL_Rect * rect)604 SDL_IBus_UpdateTextRect(SDL_Rect *rect)
605 {
606     SDL_Window *focused_win;
607     SDL_SysWMinfo info;
608     int x = 0, y = 0;
609     SDL_DBusContext *dbus;
610 
611     if (rect) {
612         SDL_memcpy(&ibus_cursor_rect, rect, sizeof(ibus_cursor_rect));
613     }
614 
615     focused_win = SDL_GetKeyboardFocus();
616     if (!focused_win) {
617         return;
618     }
619 
620     SDL_VERSION(&info.version);
621     if (!SDL_GetWindowWMInfo(focused_win, &info)) {
622         return;
623     }
624 
625     SDL_GetWindowPosition(focused_win, &x, &y);
626 
627 #if SDL_VIDEO_DRIVER_X11
628     if (info.subsystem == SDL_SYSWM_X11) {
629         SDL_DisplayData *displaydata = (SDL_DisplayData *) SDL_GetDisplayForWindow(focused_win)->driverdata;
630 
631         Display *x_disp = info.info.x11.display;
632         Window x_win = info.info.x11.window;
633         int x_screen = displaydata->screen;
634         Window unused;
635 
636         X11_XTranslateCoordinates(x_disp, x_win, RootWindow(x_disp, x_screen), 0, 0, &x, &y, &unused);
637     }
638 #endif
639 
640     x += ibus_cursor_rect.x;
641     y += ibus_cursor_rect.y;
642 
643     dbus = SDL_DBus_GetContext();
644 
645     if (IBus_CheckConnection(dbus)) {
646         DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
647                                                          input_ctx_path,
648                                                          IBUS_INPUT_INTERFACE,
649                                                          "SetCursorLocation");
650         if (msg) {
651             dbus->message_append_args(msg,
652                                       DBUS_TYPE_INT32, &x,
653                                       DBUS_TYPE_INT32, &y,
654                                       DBUS_TYPE_INT32, &ibus_cursor_rect.w,
655                                       DBUS_TYPE_INT32, &ibus_cursor_rect.h,
656                                       DBUS_TYPE_INVALID);
657         }
658 
659         if (msg) {
660             if (dbus->connection_send(ibus_conn, msg, NULL)) {
661                 dbus->connection_flush(ibus_conn);
662             }
663             dbus->message_unref(msg);
664         }
665     }
666 }
667 
668 void
SDL_IBus_PumpEvents(void)669 SDL_IBus_PumpEvents(void)
670 {
671     SDL_DBusContext *dbus = SDL_DBus_GetContext();
672 
673     if (IBus_CheckConnection(dbus)) {
674         dbus->connection_read_write(ibus_conn, 0);
675 
676         while (dbus->connection_dispatch(ibus_conn) == DBUS_DISPATCH_DATA_REMAINS) {
677             /* Do nothing, actual work happens in IBus_MessageHandler */
678         }
679     }
680 }
681 
682 #endif
683