Coverage Report

Created: 2025-07-01 06:46

/src/FreeRDP/winpr/libwinpr/comm/comm_io.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * WinPR: Windows Portable Runtime
3
 * Serial Communication API
4
 *
5
 * Copyright 2014 Hewlett-Packard Development Company, L.P.
6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
20
#include <winpr/config.h>
21
22
#include <winpr/assert.h>
23
#include <errno.h>
24
#include <termios.h>
25
#include <unistd.h>
26
27
#include <winpr/io.h>
28
#include <winpr/wlog.h>
29
#include <winpr/wtypes.h>
30
31
#include "comm.h"
32
33
BOOL _comm_set_permissive(HANDLE hDevice, BOOL permissive)
34
0
{
35
0
  WINPR_COMM* pComm = (WINPR_COMM*)hDevice;
36
37
0
  if (!CommIsHandled(hDevice))
38
0
    return FALSE;
39
40
0
  pComm->permissive = permissive;
41
0
  return TRUE;
42
0
}
43
44
/* Computes VTIME in deciseconds from Ti in milliseconds */
45
static UCHAR svtime(ULONG Ti)
46
0
{
47
  /* FIXME: look for an equivalent math function otherwise let
48
   * do the compiler do the optimization */
49
0
  if (Ti == 0)
50
0
    return 0;
51
0
  else if (Ti < 100)
52
0
    return 1;
53
0
  else if (Ti > 25500)
54
0
    return 255; /* 0xFF */
55
0
  else
56
0
    return (UCHAR)(Ti / 100);
57
0
}
58
59
/**
60
 * ERRORS:
61
 *   ERROR_INVALID_HANDLE
62
 *   ERROR_NOT_SUPPORTED
63
 *   ERROR_INVALID_PARAMETER
64
 *   ERROR_TIMEOUT
65
 *   ERROR_IO_DEVICE
66
 *   ERROR_BAD_DEVICE
67
 */
68
BOOL CommReadFile(HANDLE hDevice, LPVOID lpBuffer, DWORD nNumberOfBytesToRead,
69
                  LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped)
70
0
{
71
0
  WINPR_COMM* pComm = (WINPR_COMM*)hDevice;
72
0
  int biggestFd = -1;
73
0
  fd_set read_set;
74
0
  int nbFds = 0;
75
0
  COMMTIMEOUTS* pTimeouts = NULL;
76
0
  UCHAR vmin = 0;
77
0
  UCHAR vtime = 0;
78
0
  LONGLONG Tmax = 0;
79
0
  struct timeval tmaxTimeout;
80
0
  struct timeval* pTmaxTimeout = NULL;
81
0
  struct termios currentTermios;
82
0
  EnterCriticalSection(&pComm->ReadLock); /* KISSer by the function's beginning */
83
84
0
  if (!CommIsHandled(hDevice))
85
0
    goto return_false;
86
87
0
  if (lpOverlapped != NULL)
88
0
  {
89
0
    SetLastError(ERROR_NOT_SUPPORTED);
90
0
    goto return_false;
91
0
  }
92
93
0
  if (lpNumberOfBytesRead == NULL)
94
0
  {
95
0
    SetLastError(ERROR_INVALID_PARAMETER); /* since we doesn't support lpOverlapped != NULL */
96
0
    goto return_false;
97
0
  }
98
99
0
  *lpNumberOfBytesRead = 0; /* will be adjusted if required ... */
100
101
0
  if (nNumberOfBytesToRead <= 0) /* N */
102
0
  {
103
0
    goto return_true; /* FIXME: or FALSE? */
104
0
  }
105
106
0
  if (tcgetattr(pComm->fd, &currentTermios) < 0)
107
0
  {
108
0
    SetLastError(ERROR_IO_DEVICE);
109
0
    goto return_false;
110
0
  }
111
112
0
  if (currentTermios.c_lflag & ICANON)
113
0
  {
114
0
    CommLog_Print(WLOG_WARN, "Canonical mode not supported"); /* the timeout could not be set */
115
0
    SetLastError(ERROR_NOT_SUPPORTED);
116
0
    goto return_false;
117
0
  }
118
119
  /* http://msdn.microsoft.com/en-us/library/hh439614%28v=vs.85%29.aspx
120
   * http://msdn.microsoft.com/en-us/library/windows/hardware/hh439614%28v=vs.85%29.aspx
121
   *
122
   * ReadIntervalTimeout  | ReadTotalTimeoutMultiplier | ReadTotalTimeoutConstant | VMIN | VTIME |
123
   * TMAX  | 0            |            0               |           0              |   N  |   0   |
124
   * INDEF | Blocks for N bytes available. 0< Ti <MAXULONG  |            0               | 0 |
125
   * N  |   Ti  | INDEF | Blocks on first byte, then use Ti between bytes. MAXULONG       | 0 | 0
126
   * |   0  |   0   |   0   | Returns immediately with bytes available (don't block) MAXULONG |
127
   * MAXULONG           |      0< Tc <MAXULONG     |   N  |   0   |   Tc  | Blocks on first byte
128
   * during Tc or returns immediately with bytes available MAXULONG       |            m |
129
   * MAXULONG          |                      | Invalid 0            |            m |      0< Tc
130
   * <MAXULONG     |   N  |   0   |  Tmax | Blocks on first byte during Tmax or returns
131
   * immediately with bytes available 0< Ti <MAXULONG    |            m               |      0<
132
   * Tc <MAXULONG     |   N  |   Ti  |  Tmax | Blocks on first byte, then use Ti between bytes.
133
   * Tmax is used for the whole system call.
134
   */
135
  /* NB: timeouts are in milliseconds, VTIME are in deciseconds and is an unsigned char */
136
  /* FIXME: double check whether open(pComm->fd_read_event, O_NONBLOCK) doesn't conflict with
137
   * above use cases */
138
0
  pTimeouts = &(pComm->timeouts);
139
140
0
  if ((pTimeouts->ReadIntervalTimeout == MAXULONG) &&
141
0
      (pTimeouts->ReadTotalTimeoutConstant == MAXULONG))
142
0
  {
143
0
    CommLog_Print(
144
0
        WLOG_WARN,
145
0
        "ReadIntervalTimeout and ReadTotalTimeoutConstant cannot be both set to MAXULONG");
146
0
    SetLastError(ERROR_INVALID_PARAMETER);
147
0
    goto return_false;
148
0
  }
149
150
  /* VMIN */
151
152
0
  if ((pTimeouts->ReadIntervalTimeout == MAXULONG) &&
153
0
      (pTimeouts->ReadTotalTimeoutMultiplier == 0) && (pTimeouts->ReadTotalTimeoutConstant == 0))
154
0
  {
155
0
    vmin = 0;
156
0
  }
157
0
  else
158
0
  {
159
    /* N */
160
    /* vmin = nNumberOfBytesToRead < 256 ? nNumberOfBytesToRead : 255;*/ /* 0xFF */
161
    /* NB: we might wait endlessly with vmin=N, prefer to
162
     * force vmin=1 and return with bytes
163
     * available. FIXME: is a feature disarded here? */
164
0
    vmin = 1;
165
0
  }
166
167
  /* VTIME */
168
169
0
  if ((pTimeouts->ReadIntervalTimeout > 0) && (pTimeouts->ReadIntervalTimeout < MAXULONG))
170
0
  {
171
    /* Ti */
172
0
    vtime = svtime(pTimeouts->ReadIntervalTimeout);
173
0
  }
174
175
  /* TMAX */
176
0
  pTmaxTimeout = &tmaxTimeout;
177
178
0
  if ((pTimeouts->ReadIntervalTimeout == MAXULONG) &&
179
0
      (pTimeouts->ReadTotalTimeoutMultiplier == MAXULONG))
180
0
  {
181
    /* Tc */
182
0
    Tmax = pTimeouts->ReadTotalTimeoutConstant;
183
0
  }
184
0
  else
185
0
  {
186
    /* Tmax */
187
0
    Tmax = 1ll * nNumberOfBytesToRead * pTimeouts->ReadTotalTimeoutMultiplier +
188
0
           1ll * pTimeouts->ReadTotalTimeoutConstant;
189
190
    /* INDEFinitely */
191
0
    if ((Tmax == 0) && (pTimeouts->ReadIntervalTimeout < MAXULONG) &&
192
0
        (pTimeouts->ReadTotalTimeoutMultiplier == 0))
193
0
      pTmaxTimeout = NULL;
194
0
  }
195
196
0
  if ((currentTermios.c_cc[VMIN] != vmin) || (currentTermios.c_cc[VTIME] != vtime))
197
0
  {
198
0
    currentTermios.c_cc[VMIN] = vmin;
199
0
    currentTermios.c_cc[VTIME] = vtime;
200
201
0
    if (tcsetattr(pComm->fd, TCSANOW, &currentTermios) < 0)
202
0
    {
203
0
      CommLog_Print(WLOG_WARN,
204
0
                    "CommReadFile failure, could not apply new timeout values: VMIN=%" PRIu8
205
0
                    ", VTIME=%" PRIu8 "",
206
0
                    vmin, vtime);
207
0
      SetLastError(ERROR_IO_DEVICE);
208
0
      goto return_false;
209
0
    }
210
0
  }
211
212
  /* wait indefinitely if pTmaxTimeout is NULL */
213
214
0
  if (pTmaxTimeout != NULL)
215
0
  {
216
0
    ZeroMemory(pTmaxTimeout, sizeof(struct timeval));
217
218
0
    if (Tmax > 0) /* return immdiately if Tmax == 0 */
219
0
    {
220
0
      pTmaxTimeout->tv_sec = Tmax / 1000;           /* s */
221
0
      pTmaxTimeout->tv_usec = (Tmax % 1000) * 1000; /* us */
222
0
    }
223
0
  }
224
225
  /* FIXME: had expected eventfd_write() to return EAGAIN when
226
   * there is no eventfd_read() but this not the case. */
227
  /* discard a possible and no more relevant event */
228
0
#if defined(WINPR_HAVE_SYS_EVENTFD_H)
229
0
  {
230
0
    eventfd_t val = 0;
231
0
    (void)eventfd_read(pComm->fd_read_event, &val);
232
0
  }
233
0
#endif
234
0
  biggestFd = pComm->fd_read;
235
236
0
  if (pComm->fd_read_event > biggestFd)
237
0
    biggestFd = pComm->fd_read_event;
238
239
0
  FD_ZERO(&read_set);
240
0
  WINPR_ASSERT(pComm->fd_read_event < FD_SETSIZE);
241
0
  WINPR_ASSERT(pComm->fd_read < FD_SETSIZE);
242
0
  FD_SET(pComm->fd_read_event, &read_set);
243
0
  FD_SET(pComm->fd_read, &read_set);
244
0
  nbFds = select(biggestFd + 1, &read_set, NULL, NULL, pTmaxTimeout);
245
246
0
  if (nbFds < 0)
247
0
  {
248
0
    char ebuffer[256] = { 0 };
249
0
    CommLog_Print(WLOG_WARN, "select() failure, errno=[%d] %s\n", errno,
250
0
                  winpr_strerror(errno, ebuffer, sizeof(ebuffer)));
251
0
    SetLastError(ERROR_IO_DEVICE);
252
0
    goto return_false;
253
0
  }
254
255
0
  if (nbFds == 0)
256
0
  {
257
    /* timeout */
258
0
    SetLastError(ERROR_TIMEOUT);
259
0
    goto return_false;
260
0
  }
261
262
  /* read_set */
263
264
0
  if (FD_ISSET(pComm->fd_read_event, &read_set))
265
0
  {
266
0
#if defined(WINPR_HAVE_SYS_EVENTFD_H)
267
0
    eventfd_t event = 0;
268
269
0
    if (eventfd_read(pComm->fd_read_event, &event) < 0)
270
0
    {
271
0
      if (errno == EAGAIN)
272
0
      {
273
0
        WINPR_ASSERT(FALSE); /* not quite sure this should ever happen */
274
                             /* keep on */
275
0
      }
276
0
      else
277
0
      {
278
0
        char ebuffer[256] = { 0 };
279
0
        CommLog_Print(WLOG_WARN,
280
0
                      "unexpected error on reading fd_read_event, errno=[%d] %s\n", errno,
281
0
                      winpr_strerror(errno, ebuffer, sizeof(ebuffer)));
282
        /* FIXME: goto return_false ? */
283
0
      }
284
285
0
      WINPR_ASSERT(errno == EAGAIN);
286
0
    }
287
288
0
    if (event == WINPR_PURGE_RXABORT)
289
0
    {
290
0
      SetLastError(ERROR_CANCELLED);
291
0
      goto return_false;
292
0
    }
293
294
0
    WINPR_ASSERT(event == WINPR_PURGE_RXABORT); /* no other expected event so far */
295
0
#endif
296
0
  }
297
298
0
  if (FD_ISSET(pComm->fd_read, &read_set))
299
0
  {
300
0
    ssize_t nbRead = read(pComm->fd_read, lpBuffer, nNumberOfBytesToRead);
301
302
0
    if ((nbRead < 0) || (nbRead > nNumberOfBytesToRead))
303
0
    {
304
0
      char ebuffer[256] = { 0 };
305
0
      CommLog_Print(WLOG_WARN,
306
0
                    "CommReadFile failed, ReadIntervalTimeout=%" PRIu32
307
0
                    ", ReadTotalTimeoutMultiplier=%" PRIu32
308
0
                    ", ReadTotalTimeoutConstant=%" PRIu32 " VMIN=%u, VTIME=%u",
309
0
                    pTimeouts->ReadIntervalTimeout, pTimeouts->ReadTotalTimeoutMultiplier,
310
0
                    pTimeouts->ReadTotalTimeoutConstant, currentTermios.c_cc[VMIN],
311
0
                    currentTermios.c_cc[VTIME]);
312
0
      CommLog_Print(
313
0
          WLOG_WARN, "CommReadFile failed, nNumberOfBytesToRead=%" PRIu32 ", errno=[%d] %s",
314
0
          nNumberOfBytesToRead, errno, winpr_strerror(errno, ebuffer, sizeof(ebuffer)));
315
316
0
      if (errno == EAGAIN)
317
0
      {
318
        /* keep on */
319
0
        goto return_true; /* expect a read-loop to be implemented on the server side */
320
0
      }
321
0
      else if (errno == EBADF)
322
0
      {
323
0
        SetLastError(ERROR_BAD_DEVICE); /* STATUS_INVALID_DEVICE_REQUEST */
324
0
        goto return_false;
325
0
      }
326
0
      else
327
0
      {
328
0
        WINPR_ASSERT(FALSE);
329
0
        SetLastError(ERROR_IO_DEVICE);
330
0
        goto return_false;
331
0
      }
332
0
    }
333
334
0
    if (nbRead == 0)
335
0
    {
336
      /* termios timeout */
337
0
      SetLastError(ERROR_TIMEOUT);
338
0
      goto return_false;
339
0
    }
340
341
0
    *lpNumberOfBytesRead = WINPR_ASSERTING_INT_CAST(UINT32, nbRead);
342
343
0
    EnterCriticalSection(&pComm->EventsLock);
344
0
    if (pComm->PendingEvents & SERIAL_EV_WINPR_WAITING)
345
0
    {
346
0
      if (pComm->eventChar != '\0' &&
347
0
          memchr(lpBuffer, pComm->eventChar, WINPR_ASSERTING_INT_CAST(size_t, nbRead)))
348
0
        pComm->PendingEvents |= SERIAL_EV_RXCHAR;
349
0
    }
350
0
    LeaveCriticalSection(&pComm->EventsLock);
351
0
    goto return_true;
352
0
  }
353
354
0
  WINPR_ASSERT(FALSE);
355
0
  *lpNumberOfBytesRead = 0;
356
0
return_false:
357
0
  LeaveCriticalSection(&pComm->ReadLock);
358
0
  return FALSE;
359
0
return_true:
360
0
  LeaveCriticalSection(&pComm->ReadLock);
361
0
  return TRUE;
362
0
}
363
364
/**
365
 * ERRORS:
366
 *   ERROR_INVALID_HANDLE
367
 *   ERROR_NOT_SUPPORTED
368
 *   ERROR_INVALID_PARAMETER
369
 *   ERROR_BAD_DEVICE
370
 */
371
BOOL CommWriteFile(HANDLE hDevice, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite,
372
                   LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped)
373
0
{
374
0
  WINPR_COMM* pComm = (WINPR_COMM*)hDevice;
375
0
  struct timeval tmaxTimeout;
376
0
  struct timeval* pTmaxTimeout = NULL;
377
0
  EnterCriticalSection(&pComm->WriteLock); /* KISSer by the function's beginning */
378
379
0
  if (!CommIsHandled(hDevice))
380
0
    goto return_false;
381
382
0
  if (lpOverlapped != NULL)
383
0
  {
384
0
    SetLastError(ERROR_NOT_SUPPORTED);
385
0
    goto return_false;
386
0
  }
387
388
0
  if (lpNumberOfBytesWritten == NULL)
389
0
  {
390
0
    SetLastError(ERROR_INVALID_PARAMETER); /* since we doesn't support lpOverlapped != NULL */
391
0
    goto return_false;
392
0
  }
393
394
0
  *lpNumberOfBytesWritten = 0; /* will be adjusted if required ... */
395
396
0
  if (nNumberOfBytesToWrite <= 0)
397
0
  {
398
0
    goto return_true; /* FIXME: or FALSE? */
399
0
  }
400
401
  /* FIXME: had expected eventfd_write() to return EAGAIN when
402
   * there is no eventfd_read() but this not the case. */
403
  /* discard a possible and no more relevant event */
404
405
0
#if defined(WINPR_HAVE_SYS_EVENTFD_H)
406
0
  {
407
0
    eventfd_t val = 0;
408
0
    (void)eventfd_read(pComm->fd_write_event, &val);
409
0
  }
410
0
#endif
411
412
  /* ms */
413
0
  LONGLONG Tmax = 1ll * nNumberOfBytesToWrite * pComm->timeouts.WriteTotalTimeoutMultiplier +
414
0
                  1ll * pComm->timeouts.WriteTotalTimeoutConstant;
415
  /* NB: select() may update the timeout argument to indicate
416
   * how much time was left. Keep the timeout variable out of
417
   * the while() */
418
0
  pTmaxTimeout = &tmaxTimeout;
419
0
  ZeroMemory(pTmaxTimeout, sizeof(struct timeval));
420
421
0
  if (Tmax > 0)
422
0
  {
423
0
    pTmaxTimeout->tv_sec = Tmax / 1000;           /* s */
424
0
    pTmaxTimeout->tv_usec = (Tmax % 1000) * 1000; /* us */
425
0
  }
426
0
  else if ((pComm->timeouts.WriteTotalTimeoutMultiplier == 0) &&
427
0
           (pComm->timeouts.WriteTotalTimeoutConstant == 0))
428
0
  {
429
0
    pTmaxTimeout = NULL;
430
0
  }
431
432
  /* else return immdiately */
433
434
0
  while (*lpNumberOfBytesWritten < nNumberOfBytesToWrite)
435
0
  {
436
0
    int biggestFd = -1;
437
0
    fd_set event_set;
438
0
    fd_set write_set;
439
0
    int nbFds = 0;
440
0
    biggestFd = pComm->fd_write;
441
442
0
    if (pComm->fd_write_event > biggestFd)
443
0
      biggestFd = pComm->fd_write_event;
444
445
0
    FD_ZERO(&event_set);
446
0
    FD_ZERO(&write_set);
447
0
    WINPR_ASSERT(pComm->fd_write_event < FD_SETSIZE);
448
0
    WINPR_ASSERT(pComm->fd_write < FD_SETSIZE);
449
0
    FD_SET(pComm->fd_write_event, &event_set);
450
0
    FD_SET(pComm->fd_write, &write_set);
451
0
    nbFds = select(biggestFd + 1, &event_set, &write_set, NULL, pTmaxTimeout);
452
453
0
    if (nbFds < 0)
454
0
    {
455
0
      char ebuffer[256] = { 0 };
456
0
      CommLog_Print(WLOG_WARN, "select() failure, errno=[%d] %s\n", errno,
457
0
                    winpr_strerror(errno, ebuffer, sizeof(ebuffer)));
458
0
      SetLastError(ERROR_IO_DEVICE);
459
0
      goto return_false;
460
0
    }
461
462
0
    if (nbFds == 0)
463
0
    {
464
      /* timeout */
465
0
      SetLastError(ERROR_TIMEOUT);
466
0
      goto return_false;
467
0
    }
468
469
    /* event_set */
470
471
0
    if (FD_ISSET(pComm->fd_write_event, &event_set))
472
0
    {
473
0
#if defined(WINPR_HAVE_SYS_EVENTFD_H)
474
0
      eventfd_t event = 0;
475
476
0
      if (eventfd_read(pComm->fd_write_event, &event) < 0)
477
0
      {
478
0
        if (errno == EAGAIN)
479
0
        {
480
0
          WINPR_ASSERT(FALSE); /* not quite sure this should ever happen */
481
                               /* keep on */
482
0
        }
483
0
        else
484
0
        {
485
0
          char ebuffer[256] = { 0 };
486
0
          CommLog_Print(WLOG_WARN,
487
0
                        "unexpected error on reading fd_write_event, errno=[%d] %s\n",
488
0
                        errno, winpr_strerror(errno, ebuffer, sizeof(ebuffer)));
489
          /* FIXME: goto return_false ? */
490
0
        }
491
492
0
        WINPR_ASSERT(errno == EAGAIN);
493
0
      }
494
495
0
      if (event == WINPR_PURGE_TXABORT)
496
0
      {
497
0
        SetLastError(ERROR_CANCELLED);
498
0
        goto return_false;
499
0
      }
500
501
0
      WINPR_ASSERT(event == WINPR_PURGE_TXABORT); /* no other expected event so far */
502
0
#endif
503
0
    }
504
505
    /* write_set */
506
507
0
    if (FD_ISSET(pComm->fd_write, &write_set))
508
0
    {
509
0
      ssize_t nbWritten = 0;
510
0
      const BYTE* ptr = lpBuffer;
511
0
      nbWritten = write(pComm->fd_write, &ptr[*lpNumberOfBytesWritten],
512
0
                        nNumberOfBytesToWrite - (*lpNumberOfBytesWritten));
513
514
0
      if (nbWritten < 0)
515
0
      {
516
0
        char ebuffer[256] = { 0 };
517
0
        CommLog_Print(WLOG_WARN,
518
0
                      "CommWriteFile failed after %" PRIu32
519
0
                      " bytes written, errno=[%d] %s\n",
520
0
                      *lpNumberOfBytesWritten, errno,
521
0
                      winpr_strerror(errno, ebuffer, sizeof(ebuffer)));
522
523
0
        if (errno == EAGAIN)
524
0
        {
525
          /* keep on */
526
0
          continue;
527
0
        }
528
0
        else if (errno == EBADF)
529
0
        {
530
0
          SetLastError(ERROR_BAD_DEVICE); /* STATUS_INVALID_DEVICE_REQUEST */
531
0
          goto return_false;
532
0
        }
533
0
        else
534
0
        {
535
0
          WINPR_ASSERT(FALSE);
536
0
          SetLastError(ERROR_IO_DEVICE);
537
0
          goto return_false;
538
0
        }
539
0
      }
540
541
0
      *lpNumberOfBytesWritten += nbWritten;
542
0
    }
543
0
  } /* while */
544
545
  /* FIXME: this call to tcdrain() doesn't look correct and
546
   * might hide a bug but was required while testing a serial
547
   * printer. Its driver was expecting the modem line status
548
   * SERIAL_MSR_DSR true after the sending which was never
549
   * happening otherwise. A purge was also done before each
550
   * Write operation. The serial port was opened with:
551
   * DesiredAccess=0x0012019F. The printer worked fine with
552
   * mstsc. */
553
0
  tcdrain(pComm->fd_write);
554
555
0
return_true:
556
0
  LeaveCriticalSection(&pComm->WriteLock);
557
0
  return TRUE;
558
559
0
return_false:
560
0
  LeaveCriticalSection(&pComm->WriteLock);
561
0
  return FALSE;
562
0
}