* Sync with recent trunk (r52637).
[reactos.git] / base / applications / network / ping / ping.c
1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS ping utility
4 * FILE: base/applications/network/ping/ping.c
5 * PURPOSE: Network test utility
6 * PROGRAMMERS:
7 */
8
9 #include <winsock2.h>
10 #include <ws2tcpip.h>
11 #include <tchar.h>
12 #include <stdarg.h>
13 #include <string.h>
14 #include <stdio.h>
15 #include "resource.h"
16
17 #define NDEBUG
18
19 /* General ICMP constants */
20 #define ICMP_MINSIZE 8 /* Minimum ICMP packet size */
21 #define ICMP_MAXSIZE 65535 /* Maximum ICMP packet size */
22
23 /* ICMP message types */
24 #define ICMPMSG_ECHOREQUEST 8 /* ICMP ECHO request message */
25 #define ICMPMSG_ECHOREPLY 0 /* ICMP ECHO reply message */
26
27 #pragma pack(4)
28
29 /* IPv4 header structure */
30 typedef struct _IPv4_HEADER
31 {
32 unsigned char IHL:4;
33 unsigned char Version:4;
34 unsigned char TOS;
35 unsigned short Length;
36 unsigned short Id;
37 unsigned short FragFlags;
38 unsigned char TTL;
39 unsigned char Protocol;
40 unsigned short Checksum;
41 unsigned int SrcAddress;
42 unsigned int DstAddress;
43 } IPv4_HEADER, *PIPv4_HEADER;
44
45 /* ICMP echo request/reply header structure */
46 typedef struct _ICMP_HEADER
47 {
48 unsigned char Type;
49 unsigned char Code;
50 unsigned short Checksum;
51 unsigned short Id;
52 unsigned short SeqNum;
53 } ICMP_HEADER, *PICMP_HEADER;
54
55 typedef struct _ICMP_ECHO_PACKET
56 {
57 ICMP_HEADER Icmp;
58 } ICMP_ECHO_PACKET, *PICMP_ECHO_PACKET;
59
60 #pragma pack(1)
61
62 BOOL NeverStop;
63 BOOL ResolveAddresses;
64 UINT PingCount;
65 UINT DataSize; /* ICMP echo request data size */
66 BOOL DontFragment;
67 ULONG TTLValue;
68 ULONG TOSValue;
69 ULONG Timeout;
70 WCHAR TargetName[256];
71 SOCKET IcmpSock;
72 SOCKADDR_IN Target;
73 WCHAR TargetIP[16];
74 FD_SET Fds;
75 TIMEVAL Timeval;
76 UINT CurrentSeqNum;
77 UINT SentCount;
78 UINT LostCount;
79 BOOL MinRTTSet;
80 LARGE_INTEGER MinRTT; /* Minimum round trip time in microseconds */
81 LARGE_INTEGER MaxRTT;
82 LARGE_INTEGER SumRTT;
83 LARGE_INTEGER AvgRTT;
84 LARGE_INTEGER TicksPerMs; /* Ticks per millisecond */
85 LARGE_INTEGER TicksPerUs; /* Ticks per microsecond */
86 LARGE_INTEGER SentTime;
87 BOOL UsePerformanceCounter;
88 HANDLE hStdOutput;
89
90 #ifndef NDEBUG
91 /* Display the contents of a buffer */
92 static VOID DisplayBuffer(
93 PVOID Buffer,
94 DWORD Size)
95 {
96 UINT i;
97 PCHAR p;
98
99 printf("Buffer (0x%p) Size (0x%lX).\n", Buffer, Size);
100
101 p = (PCHAR)Buffer;
102 for (i = 0; i < Size; i++)
103 {
104 if (i % 16 == 0)
105 printf("\n");
106 printf("%02X ", (p[i]) & 0xFF);
107 }
108 }
109 #endif /* !NDEBUG */
110
111 LPWSTR
112 MyLoadString(UINT uID)
113 {
114 HRSRC hres;
115 HGLOBAL hResData;
116 WCHAR *pwsz;
117 UINT string_num, i;
118
119 hres = FindResourceW(NULL, MAKEINTRESOURCEW((LOWORD(uID) >> 4) + 1), RT_STRING);
120 if (!hres) return NULL;
121
122 hResData = LoadResource(NULL, hres);
123 if (!hResData) return NULL;
124
125 pwsz = LockResource(hResData);
126 if (!pwsz) return NULL;
127
128 string_num = uID & 15;
129 for (i = 0; i < string_num; i++)
130 pwsz += *pwsz + 1;
131
132 return pwsz + 1;
133 }
134
135 void FormatOutput(UINT uID, ...)
136 {
137 va_list valist;
138
139 WCHAR Buf[1024];
140 LPWSTR pBuf = Buf;
141 LPWSTR Format;
142 DWORD written;
143 UINT DataLength;
144
145 va_start(valist, uID);
146
147 Format = MyLoadString(uID);
148 if (!Format) return;
149
150 DataLength = FormatMessage(FORMAT_MESSAGE_FROM_STRING, Format, 0, 0, Buf,\
151 sizeof(Buf) / sizeof(WCHAR), &valist);
152
153 if(!DataLength)
154 {
155 if(GetLastError() != ERROR_INSUFFICIENT_BUFFER)
156 return;
157
158 DataLength = FormatMessage(FORMAT_MESSAGE_FROM_STRING |\
159 FORMAT_MESSAGE_ALLOCATE_BUFFER,\
160 Format, 0, 0, (LPWSTR)&pBuf, 0, &valist);
161
162 if(!DataLength)
163 return;
164 }
165
166 WriteConsole(hStdOutput, pBuf, DataLength, &written, NULL);
167
168 if(pBuf != Buf)
169 LocalFree(pBuf);
170 }
171
172 /* Display usage information on screen */
173 static VOID Usage(VOID)
174 {
175 FormatOutput(IDS_USAGE);
176 }
177
178 /* Reset configuration to default values */
179 static VOID Reset(VOID)
180 {
181 LARGE_INTEGER PerformanceCounterFrequency;
182
183 NeverStop = FALSE;
184 ResolveAddresses = FALSE;
185 PingCount = 4;
186 DataSize = 32;
187 DontFragment = FALSE;
188 TTLValue = 128;
189 TOSValue = 0;
190 Timeout = 1000;
191 UsePerformanceCounter = QueryPerformanceFrequency(&PerformanceCounterFrequency);
192
193 if (UsePerformanceCounter)
194 {
195 /* Performance counters may return incorrect results on some multiprocessor
196 platforms so we restrict execution on the first processor. This may fail
197 on Windows NT so we fall back to GetCurrentTick() for timing */
198 if (SetThreadAffinityMask (GetCurrentThread(), 1) == 0)
199 UsePerformanceCounter = FALSE;
200
201 /* Convert frequency to ticks per millisecond */
202 TicksPerMs.QuadPart = PerformanceCounterFrequency.QuadPart / 1000;
203 /* And to ticks per microsecond */
204 TicksPerUs.QuadPart = PerformanceCounterFrequency.QuadPart / 1000000;
205 }
206 if (!UsePerformanceCounter)
207 {
208 /* 1 tick per millisecond for GetCurrentTick() */
209 TicksPerMs.QuadPart = 1;
210 /* GetCurrentTick() cannot handle microseconds */
211 TicksPerUs.QuadPart = 1;
212 }
213 }
214
215 /* Parse command line parameters */
216 static BOOL ParseCmdline(int argc, LPWSTR argv[])
217 {
218 INT i;
219 BOOL FoundTarget = FALSE, InvalidOption = FALSE;
220
221 if (argc < 2)
222 {
223 Usage();
224 return FALSE;
225 }
226
227 for (i = 1; i < argc; i++)
228 {
229 if (argv[i][0] == L'-' || argv[i][0] == L'/')
230 {
231 switch (argv[i][1])
232 {
233 case L't': NeverStop = TRUE; break;
234 case L'a': ResolveAddresses = TRUE; break;
235 case L'n':
236 if (i + 1 < argc)
237 PingCount = wcstoul(argv[++i], NULL, 0);
238 else
239 InvalidOption = TRUE;
240 break;
241 case L'l':
242 if (i + 1 < argc)
243 {
244 DataSize = wcstoul(argv[++i], NULL, 0);
245
246 if (DataSize > ICMP_MAXSIZE - sizeof(ICMP_ECHO_PACKET) - sizeof(IPv4_HEADER))
247 {
248 FormatOutput(IDS_BAD_VALUE_OPTION_L, ICMP_MAXSIZE - \
249 (int)sizeof(ICMP_ECHO_PACKET) - \
250 (int)sizeof(IPv4_HEADER));
251 return FALSE;
252 }
253 } else
254 InvalidOption = TRUE;
255 break;
256 case L'f': DontFragment = TRUE; break;
257 case L'i':
258 if (i + 1 < argc)
259 TTLValue = wcstoul(argv[++i], NULL, 0);
260 else
261 InvalidOption = TRUE;
262 break;
263 case L'v':
264 if (i + 1 < argc)
265 TOSValue = wcstoul(argv[++i], NULL, 0);
266 else
267 InvalidOption = TRUE;
268 break;
269 case L'w':
270 if (i + 1 < argc)
271 Timeout = wcstoul(argv[++i], NULL, 0);
272 else
273 InvalidOption = TRUE;
274 break;
275 case '?':
276 Usage();
277 return FALSE;
278 default:
279 FormatOutput(IDS_BAD_OPTION, argv[i]);
280 return FALSE;
281 }
282 if (InvalidOption)
283 {
284 FormatOutput(IDS_BAD_OPTION_FORMAT, argv[i]);
285 return FALSE;
286 }
287 }
288 else
289 {
290 if (FoundTarget)
291 {
292 FormatOutput(IDS_BAD_PARAMETER, argv[i]);
293 return FALSE;
294 }
295 else
296 {
297 wcscpy(TargetName, argv[i]);
298 FoundTarget = TRUE;
299 }
300 }
301 }
302
303 if (!FoundTarget)
304 {
305 FormatOutput(IDS_DEST_MUST_BE_SPECIFIED);
306 return FALSE;
307 }
308
309 return TRUE;
310 }
311
312 /* Calculate checksum of data */
313 static WORD Checksum(PUSHORT data, UINT size)
314 {
315 ULONG sum = 0;
316
317 while (size > 1)
318 {
319 sum += *data++;
320 size -= sizeof(USHORT);
321 }
322
323 if (size)
324 sum += *(UCHAR*)data;
325
326 sum = (sum >> 16) + (sum & 0xFFFF);
327 sum += (sum >> 16);
328
329 return (USHORT)(~sum);
330 }
331
332 /* Prepare to ping target */
333 static BOOL Setup(VOID)
334 {
335 WORD wVersionRequested;
336 WSADATA WsaData;
337 INT Status;
338 ULONG Addr;
339 PHOSTENT phe;
340 CHAR aTargetName[256];
341
342 wVersionRequested = MAKEWORD(2, 2);
343
344 Status = WSAStartup(wVersionRequested, &WsaData);
345 if (Status != 0)
346 {
347 FormatOutput(IDS_COULD_NOT_INIT_WINSOCK);
348 return FALSE;
349 }
350
351 IcmpSock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0);
352 if (IcmpSock == INVALID_SOCKET)
353 {
354 FormatOutput(IDS_COULD_NOT_CREATE_SOCKET, WSAGetLastError());
355 return FALSE;
356 }
357
358 if (setsockopt(IcmpSock,
359 IPPROTO_IP,
360 IP_DONTFRAGMENT,
361 (const char *)&DontFragment,
362 sizeof(DontFragment)) == SOCKET_ERROR)
363 {
364 FormatOutput(IDS_SETSOCKOPT_FAILED, WSAGetLastError());
365 return FALSE;
366 }
367
368 if (setsockopt(IcmpSock,
369 IPPROTO_IP,
370 IP_TTL,
371 (const char *)&TTLValue,
372 sizeof(TTLValue)) == SOCKET_ERROR)
373 {
374 FormatOutput(IDS_SETSOCKOPT_FAILED, WSAGetLastError());
375 return FALSE;
376 }
377
378
379 if(!WideCharToMultiByte(CP_ACP, 0, TargetName, -1, aTargetName,\
380 sizeof(aTargetName), NULL, NULL))
381 {
382 FormatOutput(IDS_UNKNOWN_HOST, TargetName);
383 return FALSE;
384 }
385
386 ZeroMemory(&Target, sizeof(Target));
387 phe = NULL;
388 Addr = inet_addr(aTargetName);
389 if (Addr == INADDR_NONE)
390 {
391 phe = gethostbyname(aTargetName);
392 if (phe == NULL)
393 {
394 FormatOutput(IDS_UNKNOWN_HOST, TargetName);
395 return FALSE;
396 }
397
398 CopyMemory(&Target.sin_addr, phe->h_addr, phe->h_length);
399 Target.sin_family = phe->h_addrtype;
400 }
401 else
402 {
403 Target.sin_addr.s_addr = Addr;
404 Target.sin_family = AF_INET;
405 }
406
407
408 swprintf(TargetIP, L"%d.%d.%d.%d", Target.sin_addr.S_un.S_un_b.s_b1,\
409 Target.sin_addr.S_un.S_un_b.s_b2,\
410 Target.sin_addr.S_un.S_un_b.s_b3,\
411 Target.sin_addr.S_un.S_un_b.s_b4);
412 CurrentSeqNum = 1;
413 SentCount = 0;
414 LostCount = 0;
415 MinRTT.QuadPart = 0;
416 MaxRTT.QuadPart = 0;
417 SumRTT.QuadPart = 0;
418 MinRTTSet = FALSE;
419 return TRUE;
420 }
421
422 /* Close socket */
423 static VOID Cleanup(VOID)
424 {
425 if (IcmpSock != INVALID_SOCKET)
426 closesocket(IcmpSock);
427
428 WSACleanup();
429 }
430
431 static VOID QueryTime(PLARGE_INTEGER Time)
432 {
433 if (UsePerformanceCounter)
434 {
435 if (QueryPerformanceCounter(Time) == 0)
436 {
437 /* This should not happen, but we fall
438 back to GetCurrentTick() if it does */
439 Time->u.LowPart = (ULONG)GetTickCount();
440 Time->u.HighPart = 0;
441
442 /* 1 tick per millisecond for GetCurrentTick() */
443 TicksPerMs.QuadPart = 1;
444 /* GetCurrentTick() cannot handle microseconds */
445 TicksPerUs.QuadPart = 1;
446
447 UsePerformanceCounter = FALSE;
448 }
449 }
450 else
451 {
452 Time->u.LowPart = (ULONG)GetTickCount();
453 Time->u.HighPart = 0;
454 }
455 }
456
457 static VOID TimeToMsString(LPWSTR String, LARGE_INTEGER Time)
458 {
459 WCHAR Convstr[40];
460 LARGE_INTEGER LargeTime;
461 LPWSTR ms;
462
463 LargeTime.QuadPart = Time.QuadPart / TicksPerMs.QuadPart;
464
465 _i64tow(LargeTime.QuadPart, Convstr, 10);
466 wcscpy(String, Convstr);
467 ms = MyLoadString(IDS_MS);
468 wcscat(String, ms);
469 }
470
471 /* Locate the ICMP data and print it. Returns TRUE if the packet was good,
472 FALSE if not */
473 static BOOL DecodeResponse(PCHAR buffer, UINT size, PSOCKADDR_IN from)
474 {
475 PIPv4_HEADER IpHeader;
476 PICMP_ECHO_PACKET Icmp;
477 UINT IphLength;
478 WCHAR Time[100];
479 LARGE_INTEGER RelativeTime;
480 LARGE_INTEGER LargeTime;
481 WCHAR Sign[2];
482 WCHAR wfromIP[16];
483
484 IpHeader = (PIPv4_HEADER)buffer;
485
486 IphLength = IpHeader->IHL * 4;
487
488 if (size < IphLength + ICMP_MINSIZE)
489 {
490 #ifndef NDEBUG
491 printf("Bad size (0x%X < 0x%X)\n", size, IphLength + ICMP_MINSIZE);
492 #endif /* !NDEBUG */
493 return FALSE;
494 }
495
496 Icmp = (PICMP_ECHO_PACKET)(buffer + IphLength);
497
498 if (Icmp->Icmp.Type != ICMPMSG_ECHOREPLY)
499 {
500 #ifndef NDEBUG
501 printf("Bad ICMP type (0x%X should be 0x%X)\n", Icmp->Icmp.Type, ICMPMSG_ECHOREPLY);
502 #endif /* !NDEBUG */
503 return FALSE;
504 }
505
506 if (Icmp->Icmp.Id != (USHORT)GetCurrentProcessId())
507 {
508 #ifndef NDEBUG
509 printf("Bad ICMP id (0x%X should be 0x%X)\n", Icmp->Icmp.Id, (USHORT)GetCurrentProcessId());
510 #endif /* !NDEBUG */
511 return FALSE;
512 }
513
514 if (from->sin_addr.s_addr != Target.sin_addr.s_addr)
515 {
516 #ifndef NDEBUG
517 printf("Bad source address (%s should be %s)\n", inet_ntoa(from->sin_addr), inet_ntoa(Target.sin_addr));
518 #endif /* !NDEBUG */
519 return FALSE;
520 }
521
522 QueryTime(&LargeTime);
523
524 RelativeTime.QuadPart = (LargeTime.QuadPart - SentTime.QuadPart);
525
526 if ((RelativeTime.QuadPart / TicksPerMs.QuadPart) < 1)
527 {
528 LPWSTR ms1;
529
530 wcscpy(Sign, L"<");
531 ms1 = MyLoadString(IDS_1MS);
532 wcscpy(Time, ms1);
533 }
534 else
535 {
536 wcscpy(Sign, L"=");
537 TimeToMsString(Time, RelativeTime);
538 }
539
540
541 swprintf(wfromIP, L"%d.%d.%d.%d", from->sin_addr.S_un.S_un_b.s_b1,\
542 from->sin_addr.S_un.S_un_b.s_b2,\
543 from->sin_addr.S_un.S_un_b.s_b3,\
544 from->sin_addr.S_un.S_un_b.s_b4);
545 FormatOutput(IDS_REPLY_FROM, wfromIP,\
546 size - IphLength - (int)sizeof(ICMP_ECHO_PACKET),\
547 Sign, Time, IpHeader->TTL);
548
549 if (RelativeTime.QuadPart < MinRTT.QuadPart || !MinRTTSet)
550 {
551 MinRTT.QuadPart = RelativeTime.QuadPart;
552 MinRTTSet = TRUE;
553 }
554 if (RelativeTime.QuadPart > MaxRTT.QuadPart)
555 MaxRTT.QuadPart = RelativeTime.QuadPart;
556
557 SumRTT.QuadPart += RelativeTime.QuadPart;
558
559 return TRUE;
560 }
561
562 /* Send and receive one ping */
563 static BOOL Ping(VOID)
564 {
565 INT Status;
566 SOCKADDR From;
567 INT Length;
568 PVOID Buffer;
569 UINT Size;
570 PICMP_ECHO_PACKET Packet;
571
572 /* Account for extra space for IP header when packet is received */
573 Size = DataSize + 128;
574 Buffer = GlobalAlloc(0, Size);
575 if (!Buffer)
576 {
577 FormatOutput(IDS_NOT_ENOUGH_RESOURCES);
578 return FALSE;
579 }
580
581 ZeroMemory(Buffer, Size);
582 Packet = (PICMP_ECHO_PACKET)Buffer;
583
584 /* Assemble ICMP echo request packet */
585 Packet->Icmp.Type = ICMPMSG_ECHOREQUEST;
586 Packet->Icmp.Code = 0;
587 Packet->Icmp.Id = (USHORT)GetCurrentProcessId();
588 Packet->Icmp.SeqNum = htons((USHORT)CurrentSeqNum);
589 Packet->Icmp.Checksum = 0;
590
591 /* Calculate checksum for ICMP header and data area */
592 Packet->Icmp.Checksum = Checksum((PUSHORT)&Packet->Icmp, sizeof(ICMP_ECHO_PACKET) + DataSize);
593
594 CurrentSeqNum++;
595
596 /* Send ICMP echo request */
597
598 FD_ZERO(&Fds);
599 FD_SET(IcmpSock, &Fds);
600 Timeval.tv_sec = Timeout / 1000;
601 Timeval.tv_usec = Timeout % 1000;
602 Status = select(0, NULL, &Fds, NULL, &Timeval);
603 if ((Status != SOCKET_ERROR) && (Status != 0))
604 {
605
606 #ifndef NDEBUG
607 printf("Sending packet\n");
608 DisplayBuffer(Buffer, sizeof(ICMP_ECHO_PACKET) + DataSize);
609 printf("\n");
610 #endif /* !NDEBUG */
611
612 Status = sendto(IcmpSock, Buffer, sizeof(ICMP_ECHO_PACKET) + DataSize,
613 0, (SOCKADDR*)&Target, sizeof(Target));
614 QueryTime(&SentTime);
615 SentCount++;
616 }
617 if (Status == SOCKET_ERROR)
618 {
619 if (WSAGetLastError() == WSAEHOSTUNREACH)
620 FormatOutput(IDS_DEST_UNREACHABLE);
621 else
622 FormatOutput(IDS_COULD_NOT_TRANSMIT, WSAGetLastError());
623 GlobalFree(Buffer);
624 return FALSE;
625 }
626
627 /* Expect to receive ICMP echo reply */
628 FD_ZERO(&Fds);
629 FD_SET(IcmpSock, &Fds);
630 Timeval.tv_sec = Timeout / 1000;
631 Timeval.tv_usec = Timeout % 1000;
632
633 do {
634 Status = select(0, &Fds, NULL, NULL, &Timeval);
635 if ((Status != SOCKET_ERROR) && (Status != 0))
636 {
637 Length = sizeof(From);
638 Status = recvfrom(IcmpSock, Buffer, Size, 0, &From, &Length);
639
640 #ifndef NDEBUG
641 printf("Received packet\n");
642 DisplayBuffer(Buffer, Status);
643 printf("\n");
644 #endif /* !NDEBUG */
645 }
646 else
647 LostCount++;
648 if (Status == SOCKET_ERROR)
649 {
650 if (WSAGetLastError() != WSAETIMEDOUT)
651 {
652 FormatOutput(IDS_COULD_NOT_RECV, WSAGetLastError());
653 GlobalFree(Buffer);
654 return FALSE;
655 }
656 Status = 0;
657 }
658
659 if (Status == 0)
660 {
661 FormatOutput(IDS_REQUEST_TIMEOUT);
662 GlobalFree(Buffer);
663 return TRUE;
664 }
665
666 } while (!DecodeResponse(Buffer, Status, (PSOCKADDR_IN)&From));
667
668 GlobalFree(Buffer);
669 return TRUE;
670 }
671
672
673 /* Program entry point */
674 int wmain(int argc, LPWSTR argv[])
675 {
676 UINT Count;
677 WCHAR MinTime[20];
678 WCHAR MaxTime[20];
679 WCHAR AvgTime[20];
680
681 hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
682
683 Reset();
684
685 if ((ParseCmdline(argc, argv)) && (Setup()))
686 {
687
688 FormatOutput(IDS_PING_WITH_BYTES, TargetName, TargetIP, DataSize);
689
690 Count = 0;
691 while ((NeverStop) || (Count < PingCount))
692 {
693 Ping();
694 Count++;
695 if((NeverStop) || (Count < PingCount))
696 Sleep(Timeout);
697 };
698
699 Cleanup();
700
701 /* Calculate avarage round trip time */
702 if ((SentCount - LostCount) > 0)
703 AvgRTT.QuadPart = SumRTT.QuadPart / (SentCount - LostCount);
704 else
705 AvgRTT.QuadPart = 0;
706
707 /* Calculate loss percent */
708 Count = SentCount ? (LostCount * 100) / SentCount : 0;
709
710 if (!MinRTTSet)
711 MinRTT = MaxRTT;
712
713 TimeToMsString(MinTime, MinRTT);
714 TimeToMsString(MaxTime, MaxRTT);
715 TimeToMsString(AvgTime, AvgRTT);
716
717 /* Print statistics */
718 FormatOutput(IDS_PING_STATISTICS, TargetIP);
719 FormatOutput(IDS_PACKETS_SENT_RECEIVED_LOST,\
720 SentCount, SentCount - LostCount, LostCount, Count);
721
722
723 /* Print approximate times or NO approximate times if 100% loss */
724 if ((SentCount - LostCount) > 0)
725 {
726 FormatOutput(IDS_APPROXIMATE_ROUND_TRIP);
727 FormatOutput(IDS_MIN_MAX_AVERAGE, MinTime, MaxTime, AvgTime);
728 }
729 }
730 else
731 {
732 return 1;
733 }
734 return 0;
735 }
736
737 /* EOF */