1 /*
2     Copyright (c) 2007-2019 Contributors as noted in the AUTHORS file
3 
4     This file is part of libzmq, the ZeroMQ core engine in C++.
5 
6     libzmq is free software; you can redistribute it and/or modify it under
7     the terms of the GNU Lesser General Public License (LGPL) as published
8     by the Free Software Foundation; either version 3 of the License, or
9     (at your option) any later version.
10 
11     As a special exception, the Contributors give you permission to link
12     this library with independent modules to produce an executable,
13     regardless of the license terms of these independent modules, and to
14     copy and distribute the resulting executable under terms of your choice,
15     provided that you also meet, for each linked independent module, the
16     terms and conditions of the license of that module. An independent
17     module is a module which is not derived from or based on this library.
18     If you modify this library, you must extend this exception to your
19     version of the library.
20 
21     libzmq is distributed in the hope that it will be useful, but WITHOUT
22     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
23     FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
24     License for more details.
25 
26     You should have received a copy of the GNU Lesser General Public License
27     along with this program.  If not, see <http://www.gnu.org/licenses/>.
28 */
29 #include "testutil.hpp"
30 #include "testutil_unity.hpp"
31 
32 #include <stdarg.h>
33 #include <string.h>
34 
35 #if defined _WIN32
36 #include "../src/windows.hpp"
37 #if defined _MSC_VER
38 #if defined ZMQ_HAVE_IPC
39 #include <direct.h>
40 #include <afunix.h>
41 #endif
42 #include <crtdbg.h>
43 #pragma warning(disable : 4996)
44 // iphlpapi is needed for if_nametoindex (not on Windows XP)
45 #if _WIN32_WINNT > _WIN32_WINNT_WINXP
46 #pragma comment(lib, "iphlpapi")
47 #endif
48 #endif
49 #else
50 #include <pthread.h>
51 #include <unistd.h>
52 #include <signal.h>
53 #include <stdlib.h>
54 #include <grp.h>
55 #include <sys/wait.h>
56 #include <sys/socket.h>
57 #include <sys/types.h>
58 #include <netinet/in.h>
59 #include <arpa/inet.h>
60 #include <net/if.h>
61 #include <netdb.h>
62 #include <sys/un.h>
63 #include <dirent.h>
64 #if defined(ZMQ_HAVE_AIX)
65 #include <sys/types.h>
66 #include <sys/socketvar.h>
67 #endif
68 #endif
69 
70 #ifndef PATH_MAX
71 #define PATH_MAX 1024
72 #endif
73 
74 const char *SEQ_END = (const char *) 1;
75 
76 const char bounce_content[] = "12345678ABCDEFGH12345678abcdefgh";
77 
send_bounce_msg(void * socket_)78 static void send_bounce_msg (void *socket_)
79 {
80     send_string_expect_success (socket_, bounce_content, ZMQ_SNDMORE);
81     send_string_expect_success (socket_, bounce_content, 0);
82 }
83 
recv_bounce_msg(void * socket_)84 static void recv_bounce_msg (void *socket_)
85 {
86     recv_string_expect_success (socket_, bounce_content, 0);
87     int rcvmore;
88     size_t sz = sizeof (rcvmore);
89     TEST_ASSERT_SUCCESS_ERRNO (
90       zmq_getsockopt (socket_, ZMQ_RCVMORE, &rcvmore, &sz));
91     TEST_ASSERT_TRUE (rcvmore);
92     recv_string_expect_success (socket_, bounce_content, 0);
93     TEST_ASSERT_SUCCESS_ERRNO (
94       zmq_getsockopt (socket_, ZMQ_RCVMORE, &rcvmore, &sz));
95     TEST_ASSERT_FALSE (rcvmore);
96 }
97 
bounce(void * server_,void * client_)98 void bounce (void *server_, void *client_)
99 {
100     //  Send message from client to server
101     send_bounce_msg (client_);
102 
103     //  Receive message at server side and
104     //  check that message is still the same
105     recv_bounce_msg (server_);
106 
107     //  Send two parts back to client
108     send_bounce_msg (server_);
109 
110     //  Receive the two parts at the client side
111     recv_bounce_msg (client_);
112 }
113 
send_bounce_msg_may_fail(void * socket_)114 static void send_bounce_msg_may_fail (void *socket_)
115 {
116     int timeout = 250;
117     TEST_ASSERT_SUCCESS_ERRNO (
118       zmq_setsockopt (socket_, ZMQ_SNDTIMEO, &timeout, sizeof (int)));
119     int rc = zmq_send (socket_, bounce_content, 32, ZMQ_SNDMORE);
120     TEST_ASSERT_TRUE ((rc == 32) || ((rc == -1) && (errno == EAGAIN)));
121     rc = zmq_send (socket_, bounce_content, 32, 0);
122     TEST_ASSERT_TRUE ((rc == 32) || ((rc == -1) && (errno == EAGAIN)));
123 }
124 
recv_bounce_msg_fail(void * socket_)125 static void recv_bounce_msg_fail (void *socket_)
126 {
127     int timeout = 250;
128     char buffer[32];
129     TEST_ASSERT_SUCCESS_ERRNO (
130       zmq_setsockopt (socket_, ZMQ_RCVTIMEO, &timeout, sizeof (int)));
131     TEST_ASSERT_FAILURE_ERRNO (EAGAIN, zmq_recv (socket_, buffer, 32, 0));
132 }
133 
expect_bounce_fail(void * server_,void * client_)134 void expect_bounce_fail (void *server_, void *client_)
135 {
136     //  Send message from client to server
137     send_bounce_msg_may_fail (client_);
138 
139     //  Receive message at server side (should not succeed)
140     recv_bounce_msg_fail (server_);
141 
142     //  Send message from server to client to test other direction
143     //  If connection failed, send may block, without a timeout
144     send_bounce_msg_may_fail (server_);
145 
146     //  Receive message at client side (should not succeed)
147     recv_bounce_msg_fail (client_);
148 }
149 
s_recv(void * socket_)150 char *s_recv (void *socket_)
151 {
152     char buffer[256];
153     int size = zmq_recv (socket_, buffer, 255, 0);
154     if (size == -1)
155         return NULL;
156     if (size > 255)
157         size = 255;
158     buffer[size] = 0;
159     return strdup (buffer);
160 }
161 
s_send_seq(void * socket_,...)162 void s_send_seq (void *socket_, ...)
163 {
164     va_list ap;
165     va_start (ap, socket_);
166     const char *data = va_arg (ap, const char *);
167     while (true) {
168         const char *prev = data;
169         data = va_arg (ap, const char *);
170         bool end = data == SEQ_END;
171 
172         if (!prev) {
173             TEST_ASSERT_SUCCESS_ERRNO (
174               zmq_send (socket_, 0, 0, end ? 0 : ZMQ_SNDMORE));
175         } else {
176             TEST_ASSERT_SUCCESS_ERRNO (zmq_send (
177               socket_, prev, strlen (prev) + 1, end ? 0 : ZMQ_SNDMORE));
178         }
179         if (end)
180             break;
181     }
182     va_end (ap);
183 }
184 
s_recv_seq(void * socket_,...)185 void s_recv_seq (void *socket_, ...)
186 {
187     zmq_msg_t msg;
188     zmq_msg_init (&msg);
189 
190     int more;
191     size_t more_size = sizeof (more);
192 
193     va_list ap;
194     va_start (ap, socket_);
195     const char *data = va_arg (ap, const char *);
196 
197     while (true) {
198         TEST_ASSERT_SUCCESS_ERRNO (zmq_msg_recv (&msg, socket_, 0));
199 
200         if (!data)
201             TEST_ASSERT_EQUAL_INT (0, zmq_msg_size (&msg));
202         else
203             TEST_ASSERT_EQUAL_STRING (data, (const char *) zmq_msg_data (&msg));
204 
205         data = va_arg (ap, const char *);
206         bool end = data == SEQ_END;
207 
208         TEST_ASSERT_SUCCESS_ERRNO (
209           zmq_getsockopt (socket_, ZMQ_RCVMORE, &more, &more_size));
210 
211         TEST_ASSERT_TRUE (!more == end);
212         if (end)
213             break;
214     }
215     va_end (ap);
216 
217     zmq_msg_close (&msg);
218 }
219 
close_zero_linger(void * socket_)220 void close_zero_linger (void *socket_)
221 {
222     int linger = 0;
223     int rc = zmq_setsockopt (socket_, ZMQ_LINGER, &linger, sizeof (linger));
224     TEST_ASSERT_TRUE (rc == 0 || errno == ETERM);
225     TEST_ASSERT_SUCCESS_ERRNO (zmq_close (socket_));
226 }
227 
setup_test_environment(int timeout_seconds_)228 void setup_test_environment (int timeout_seconds_)
229 {
230 #if defined _WIN32
231 #if defined _MSC_VER
232     _set_abort_behavior (0, _WRITE_ABORT_MSG);
233     _CrtSetReportMode (_CRT_ASSERT, _CRTDBG_MODE_FILE);
234     _CrtSetReportFile (_CRT_ASSERT, _CRTDBG_FILE_STDERR);
235 #endif
236 #else
237 #if defined ZMQ_HAVE_CYGWIN
238     // abort test after 121 seconds
239     alarm (121);
240 #else
241 #if !defined ZMQ_DISABLE_TEST_TIMEOUT
242     // abort test after timeout_seconds_ seconds
243     alarm (timeout_seconds_);
244 #endif
245 #endif
246 #endif
247 #if defined __MVS__
248     // z/OS UNIX System Services: Ignore SIGPIPE during test runs, as a
249     // workaround for no SO_NOGSIGPIPE socket option.
250     signal (SIGPIPE, SIG_IGN);
251 #endif
252 }
253 
msleep(int milliseconds_)254 void msleep (int milliseconds_)
255 {
256 #ifdef ZMQ_HAVE_WINDOWS
257     Sleep (milliseconds_);
258 #else
259     usleep (static_cast<useconds_t> (milliseconds_) * 1000);
260 #endif
261 }
262 
is_ipv6_available()263 int is_ipv6_available ()
264 {
265 #if defined(ZMQ_HAVE_WINDOWS) && (_WIN32_WINNT < 0x0600)
266     return 0;
267 #else
268     int rc, ipv6 = 1;
269     struct sockaddr_in6 test_addr;
270 
271     memset (&test_addr, 0, sizeof (test_addr));
272     test_addr.sin6_family = AF_INET6;
273     inet_pton (AF_INET6, "::1", &(test_addr.sin6_addr));
274 
275     fd_t fd = socket (AF_INET6, SOCK_STREAM, IPPROTO_IP);
276     if (fd == retired_fd)
277         ipv6 = 0;
278     else {
279 #ifdef ZMQ_HAVE_WINDOWS
280         setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &ipv6,
281                     sizeof (int));
282         rc = setsockopt (fd, IPPROTO_IPV6, IPV6_V6ONLY, (const char *) &ipv6,
283                          sizeof (int));
284         if (rc == SOCKET_ERROR)
285             ipv6 = 0;
286         else {
287             rc = bind (fd, (struct sockaddr *) &test_addr, sizeof (test_addr));
288             if (rc == SOCKET_ERROR)
289                 ipv6 = 0;
290         }
291 #else
292         setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &ipv6, sizeof (int));
293         rc = setsockopt (fd, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6, sizeof (int));
294         if (rc != 0)
295             ipv6 = 0;
296         else {
297             rc = bind (fd, reinterpret_cast<struct sockaddr *> (&test_addr),
298                        sizeof (test_addr));
299             if (rc != 0)
300                 ipv6 = 0;
301         }
302 #endif
303         close (fd);
304     }
305 
306     return ipv6;
307 #endif // _WIN32_WINNT < 0x0600
308 }
309 
is_tipc_available()310 int is_tipc_available ()
311 {
312 #ifndef ZMQ_HAVE_TIPC
313     return 0;
314 #else
315     int tipc = 0;
316 
317     void *ctx = zmq_init (1);
318     TEST_ASSERT_NOT_NULL (ctx);
319     void *rep = zmq_socket (ctx, ZMQ_REP);
320     TEST_ASSERT_NOT_NULL (rep);
321     tipc = zmq_bind (rep, "tipc://{5560,0,0}");
322 
323     zmq_close (rep);
324     zmq_ctx_term (ctx);
325 
326     return tipc == 0;
327 #endif // ZMQ_HAVE_TIPC
328 }
329 
test_inet_pton(int af_,const char * src_,void * dst_)330 int test_inet_pton (int af_, const char *src_, void *dst_)
331 {
332 #if defined(ZMQ_HAVE_WINDOWS) && (_WIN32_WINNT < 0x0600)
333     if (af_ == AF_INET) {
334         struct in_addr *ip4addr = (struct in_addr *) dst_;
335 
336         ip4addr->s_addr = inet_addr (src_);
337 
338         //  INADDR_NONE is -1 which is also a valid representation for IP
339         //  255.255.255.255
340         if (ip4addr->s_addr == INADDR_NONE
341             && strcmp (src_, "255.255.255.255") != 0) {
342             return 0;
343         }
344 
345         //  Success
346         return 1;
347     } else {
348         //  Not supported.
349         return 0;
350     }
351 #else
352     return inet_pton (af_, src_, dst_);
353 #endif
354 }
355 
bind_bsd_socket(int socket_)356 sockaddr_in bind_bsd_socket (int socket_)
357 {
358     struct sockaddr_in saddr;
359     memset (&saddr, 0, sizeof (saddr));
360     saddr.sin_family = AF_INET;
361     saddr.sin_addr.s_addr = INADDR_ANY;
362 #if !defined(_WIN32_WINNT) || (_WIN32_WINNT >= 0x0600)
363     saddr.sin_port = 0;
364 #else
365     saddr.sin_port = htons (PORT_6);
366 #endif
367 
368     TEST_ASSERT_SUCCESS_RAW_ERRNO (
369       bind (socket_, (struct sockaddr *) &saddr, sizeof (saddr)));
370 
371 #if !defined(_WIN32_WINNT) || (_WIN32_WINNT >= 0x0600)
372     socklen_t saddr_len = sizeof (saddr);
373     TEST_ASSERT_SUCCESS_RAW_ERRNO (
374       getsockname (socket_, (struct sockaddr *) &saddr, &saddr_len));
375 #endif
376 
377     return saddr;
378 }
379 
connect_socket(const char * endpoint_,const int af_,const int protocol_)380 fd_t connect_socket (const char *endpoint_, const int af_, const int protocol_)
381 {
382     struct sockaddr_storage addr;
383     //  OSX is very opinionated and wants the size to match the AF family type
384     socklen_t addr_len;
385     const fd_t s_pre = socket (af_, SOCK_STREAM,
386                                protocol_ == IPPROTO_UDP
387                                  ? IPPROTO_UDP
388                                  : protocol_ == IPPROTO_TCP ? IPPROTO_TCP : 0);
389     TEST_ASSERT_NOT_EQUAL (-1, s_pre);
390 
391     if (af_ == AF_INET || af_ == AF_INET6) {
392         const char *port = strrchr (endpoint_, ':') + 1;
393         char address[MAX_SOCKET_STRING];
394         // getaddrinfo does not like [x:y::z]
395         if (*strchr (endpoint_, '/') + 2 == '[') {
396             strcpy (address, strchr (endpoint_, '[') + 1);
397             address[strlen (address) - strlen (port) - 2] = '\0';
398         } else {
399             strcpy (address, strchr (endpoint_, '/') + 2);
400             address[strlen (address) - strlen (port) - 1] = '\0';
401         }
402 
403         struct addrinfo *in, hint;
404         memset (&hint, 0, sizeof (struct addrinfo));
405         hint.ai_flags = AI_NUMERICSERV;
406         hint.ai_family = af_;
407         hint.ai_socktype = protocol_ == IPPROTO_UDP ? SOCK_DGRAM : SOCK_STREAM;
408         hint.ai_protocol = protocol_ == IPPROTO_UDP ? IPPROTO_UDP : IPPROTO_TCP;
409 
410         TEST_ASSERT_SUCCESS_RAW_ZERO_ERRNO (
411           getaddrinfo (address, port, &hint, &in));
412         TEST_ASSERT_NOT_NULL (in);
413         memcpy (&addr, in->ai_addr, in->ai_addrlen);
414         addr_len = (socklen_t) in->ai_addrlen;
415         freeaddrinfo (in);
416     } else {
417 #if defined(ZMQ_HAVE_IPC)
418         //  Cannot cast addr as gcc 4.4 will fail with strict aliasing errors
419         (*(struct sockaddr_un *) &addr).sun_family = AF_UNIX;
420         strcpy ((*(struct sockaddr_un *) &addr).sun_path, endpoint_);
421         addr_len = sizeof (struct sockaddr_un);
422 #else
423         return retired_fd;
424 #endif
425     }
426 
427     TEST_ASSERT_SUCCESS_RAW_ERRNO (
428       connect (s_pre, (struct sockaddr *) &addr, addr_len));
429 
430     return s_pre;
431 }
432 
bind_socket_resolve_port(const char * address_,const char * port_,char * my_endpoint_,const int af_,const int protocol_)433 fd_t bind_socket_resolve_port (const char *address_,
434                                const char *port_,
435                                char *my_endpoint_,
436                                const int af_,
437                                const int protocol_)
438 {
439     struct sockaddr_storage addr;
440     //  OSX is very opinionated and wants the size to match the AF family type
441     socklen_t addr_len;
442     const fd_t s_pre = socket (af_, SOCK_STREAM,
443                                protocol_ == IPPROTO_UDP
444                                  ? IPPROTO_UDP
445                                  : protocol_ == IPPROTO_TCP ? IPPROTO_TCP : 0);
446     TEST_ASSERT_NOT_EQUAL (-1, s_pre);
447 
448     if (af_ == AF_INET || af_ == AF_INET6) {
449 #ifdef ZMQ_HAVE_WINDOWS
450         const char flag = '\1';
451 #elif defined ZMQ_HAVE_VXWORKS
452         char flag = '\1';
453 #else
454         int flag = 1;
455 #endif
456         struct addrinfo *in, hint;
457         memset (&hint, 0, sizeof (struct addrinfo));
458         hint.ai_flags = AI_NUMERICSERV;
459         hint.ai_family = af_;
460         hint.ai_socktype = protocol_ == IPPROTO_UDP ? SOCK_DGRAM : SOCK_STREAM;
461         hint.ai_protocol = protocol_ == IPPROTO_UDP ? IPPROTO_UDP : IPPROTO_TCP;
462 
463         TEST_ASSERT_SUCCESS_RAW_ERRNO (
464           setsockopt (s_pre, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof (int)));
465         TEST_ASSERT_SUCCESS_RAW_ZERO_ERRNO (
466           getaddrinfo (address_, port_, &hint, &in));
467         TEST_ASSERT_NOT_NULL (in);
468         memcpy (&addr, in->ai_addr, in->ai_addrlen);
469         addr_len = (socklen_t) in->ai_addrlen;
470         freeaddrinfo (in);
471     } else {
472 #if defined(ZMQ_HAVE_IPC)
473         //  Cannot cast addr as gcc 4.4 will fail with strict aliasing errors
474         (*(struct sockaddr_un *) &addr).sun_family = AF_UNIX;
475         addr_len = sizeof (struct sockaddr_un);
476 #if defined ZMQ_HAVE_WINDOWS
477         char buffer[MAX_PATH] = "";
478 
479         TEST_ASSERT_SUCCESS_RAW_ERRNO (tmpnam_s (buffer));
480         TEST_ASSERT_SUCCESS_RAW_ERRNO (_mkdir (buffer));
481         strcat (buffer, "/ipc");
482 #else
483         char buffer[PATH_MAX] = "";
484         strcpy (buffer, "tmpXXXXXX");
485 #ifdef HAVE_MKDTEMP
486         TEST_ASSERT_TRUE (mkdtemp (buffer));
487         strcat (buffer, "/socket");
488 #else
489         int fd = mkstemp (buffer);
490         TEST_ASSERT_TRUE (fd != -1);
491         close (fd);
492 #endif
493 #endif
494         strcpy ((*(struct sockaddr_un *) &addr).sun_path, buffer);
495         memcpy (my_endpoint_, "ipc://", 7);
496         strcat (my_endpoint_, buffer);
497 
498         // TODO check return value of unlink
499         unlink (buffer);
500 #else
501         return retired_fd;
502 #endif
503     }
504 
505     TEST_ASSERT_SUCCESS_RAW_ERRNO (
506       bind (s_pre, (struct sockaddr *) &addr, addr_len));
507     TEST_ASSERT_SUCCESS_RAW_ERRNO (listen (s_pre, SOMAXCONN));
508 
509     if (af_ == AF_INET || af_ == AF_INET6) {
510         addr_len = sizeof (struct sockaddr_storage);
511         TEST_ASSERT_SUCCESS_RAW_ERRNO (
512           getsockname (s_pre, (struct sockaddr *) &addr, &addr_len));
513         sprintf (my_endpoint_, "%s://%s:%u",
514                  protocol_ == IPPROTO_TCP
515                    ? "tcp"
516                    : protocol_ == IPPROTO_UDP
517                        ? "udp"
518                        : protocol_ == IPPROTO_WSS ? "wss" : "ws",
519                  address_,
520                  af_ == AF_INET
521                    ? ntohs ((*(struct sockaddr_in *) &addr).sin_port)
522                    : ntohs ((*(struct sockaddr_in6 *) &addr).sin6_port));
523     }
524 
525     return s_pre;
526 }
527 
streq(const char * lhs_,const char * rhs_)528 bool streq (const char *lhs_, const char *rhs_)
529 {
530     return strcmp (lhs_, rhs_) == 0;
531 }
532 
strneq(const char * lhs_,const char * rhs_)533 bool strneq (const char *lhs_, const char *rhs_)
534 {
535     return strcmp (lhs_, rhs_) != 0;
536 }
537 
538 #if defined _WIN32
fuzzer_corpus_encode(const char * dirname,uint8_t *** data,size_t ** len,size_t * num_cases)539 int fuzzer_corpus_encode (const char *dirname,
540                           uint8_t ***data,
541                           size_t **len,
542                           size_t *num_cases)
543 {
544     (void) dirname;
545     (void) data;
546     (void) len;
547     (void) num_cases;
548 
549     return -1;
550 }
551 
552 #else
553 
fuzzer_corpus_encode(const char * dirname,uint8_t *** data,size_t ** len,size_t * num_cases)554 int fuzzer_corpus_encode (const char *dirname,
555                           uint8_t ***data,
556                           size_t **len,
557                           size_t *num_cases)
558 {
559     TEST_ASSERT_NOT_NULL (dirname);
560     TEST_ASSERT_NOT_NULL (data);
561     TEST_ASSERT_NOT_NULL (len);
562 
563     struct dirent *ent;
564     DIR *dir = opendir (dirname);
565     if (!dir)
566         return -1;
567 
568     *len = NULL;
569     *data = NULL;
570     *num_cases = 0;
571 
572     while ((ent = readdir (dir)) != NULL) {
573         if (!strcmp (ent->d_name, ".") || !strcmp (ent->d_name, ".."))
574             continue;
575 
576         char *filename =
577           (char *) malloc (strlen (dirname) + strlen (ent->d_name) + 2);
578         TEST_ASSERT_NOT_NULL (filename);
579         strcpy (filename, dirname);
580         strcat (filename, "/");
581         strcat (filename, ent->d_name);
582         FILE *f = fopen (filename, "r");
583         free (filename);
584         if (!f)
585             continue;
586 
587         fseek (f, 0, SEEK_END);
588         size_t file_len = ftell (f);
589         fseek (f, 0, SEEK_SET);
590         if (file_len == 0) {
591             fclose (f);
592             continue;
593         }
594 
595         *len = (size_t *) realloc (*len, (*num_cases + 1) * sizeof (size_t));
596         TEST_ASSERT_NOT_NULL (*len);
597         *(*len + *num_cases) = file_len;
598         *data =
599           (uint8_t **) realloc (*data, (*num_cases + 1) * sizeof (uint8_t *));
600         TEST_ASSERT_NOT_NULL (*data);
601         *(*data + *num_cases) =
602           (uint8_t *) malloc (file_len * sizeof (uint8_t));
603         TEST_ASSERT_NOT_NULL (*(*data + *num_cases));
604         size_t read_bytes = 0;
605         read_bytes = fread (*(*data + *num_cases), 1, file_len, f);
606         TEST_ASSERT_EQUAL (file_len, read_bytes);
607         (*num_cases)++;
608 
609         fclose (f);
610     }
611 
612     closedir (dir);
613 
614     return 0;
615 }
616 #endif
617