1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
|
/* $Id: x11-clipboard.cpp $ */
/** @file
* Shared Clipboard Service - Linux host.
*/
/*
* Copyright (C) 2006-2019 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* you can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
/*********************************************************************************************************************************
* Header Files *
*********************************************************************************************************************************/
#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD
#include <iprt/assert.h>
#include <iprt/critsect.h>
#include <iprt/env.h>
#include <iprt/mem.h>
#include <iprt/semaphore.h>
#include <iprt/string.h>
#include <VBox/GuestHost/SharedClipboard.h>
#include <VBox/HostServices/VBoxClipboardSvc.h>
#include <VBox/err.h>
#include "VBoxClipboard.h"
/*********************************************************************************************************************************
* Structures and Typedefs *
*********************************************************************************************************************************/
struct _VBOXCLIPBOARDREQFROMVBOX;
typedef struct _VBOXCLIPBOARDREQFROMVBOX VBOXCLIPBOARDREQFROMVBOX;
/** Global context information used by the host glue for the X11 clipboard
* backend */
struct _VBOXCLIPBOARDCONTEXT
{
/** This mutex is grabbed during any critical operations on the clipboard
* which might clash with others. */
RTCRITSECT clipboardMutex;
/** The currently pending request for data from VBox. NULL if there is
* no request pending. The protocol for completing a request is to grab
* the critical section, check that @a pReq is not NULL, fill in the data
* fields and set @a pReq to NULL. The protocol for cancelling a pending
* request is to grab the critical section and set pReq to NULL.
* It is an error if a request arrives while another one is pending, and
* the backend is responsible for ensuring that this does not happen. */
VBOXCLIPBOARDREQFROMVBOX *pReq;
/** Pointer to the opaque X11 backend structure */
CLIPBACKEND *pBackend;
/** Pointer to the VBox host client data structure. */
VBOXCLIPBOARDCLIENTDATA *pClient;
/** We set this when we start shutting down as a hint not to post any new
* requests. */
bool fShuttingDown;
};
/**
* Report formats available in the X11 clipboard to VBox.
* @param pCtx Opaque context pointer for the glue code
* @param u32Formats The formats available
* @note Host glue code
*/
void ClipReportX11Formats(VBOXCLIPBOARDCONTEXT *pCtx,
uint32_t u32Formats)
{
LogRelFlowFunc(("called. pCtx=%p, u32Formats=%02X\n", pCtx, u32Formats));
vboxSvcClipboardReportMsg(pCtx->pClient,
VBOX_SHARED_CLIPBOARD_HOST_MSG_FORMATS,
u32Formats);
}
/**
* Initialise the host side of the shared clipboard.
* @note Host glue code
*/
int vboxClipboardInit (void)
{
return VINF_SUCCESS;
}
/**
* Terminate the host side of the shared clipboard.
* @note host glue code
*/
void vboxClipboardDestroy (void)
{
}
/**
* Connect a guest to the shared clipboard.
* @note host glue code
* @note on the host, we assume that some other application already owns
* the clipboard and leave ownership to X11.
*/
int vboxClipboardConnect (VBOXCLIPBOARDCLIENTDATA *pClient, bool fHeadless)
{
int rc = VINF_SUCCESS;
CLIPBACKEND *pBackend = NULL;
LogRel(("Starting host clipboard service\n"));
VBOXCLIPBOARDCONTEXT *pCtx =
(VBOXCLIPBOARDCONTEXT *) RTMemAllocZ(sizeof(VBOXCLIPBOARDCONTEXT));
if (!pCtx)
rc = VERR_NO_MEMORY;
else
{
RTCritSectInit(&pCtx->clipboardMutex);
pBackend = ClipConstructX11(pCtx, fHeadless);
if (pBackend == NULL)
rc = VERR_NO_MEMORY;
else
{
pCtx->pBackend = pBackend;
pClient->pCtx = pCtx;
pCtx->pClient = pClient;
rc = ClipStartX11(pBackend, true /* grab shared clipboard */);
}
if (RT_FAILURE(rc))
RTCritSectDelete(&pCtx->clipboardMutex);
}
if (RT_FAILURE(rc))
{
RTMemFree(pCtx);
LogRel(("Failed to initialise the shared clipboard\n"));
}
LogRelFlowFunc(("returning %Rrc\n", rc));
return rc;
}
/**
* Synchronise the contents of the host clipboard with the guest, called
* after a save and restore of the guest.
* @note Host glue code
*/
int vboxClipboardSync (VBOXCLIPBOARDCLIENTDATA *pClient)
{
/* Tell the guest we have no data in case X11 is not available. If
* there is data in the host clipboard it will automatically be sent to
* the guest when the clipboard starts up. */
vboxSvcClipboardReportMsg (pClient,
VBOX_SHARED_CLIPBOARD_HOST_MSG_FORMATS, 0);
return VINF_SUCCESS;
}
/**
* Shut down the shared clipboard service and "disconnect" the guest.
* @note Host glue code
*/
void vboxClipboardDisconnect (VBOXCLIPBOARDCLIENTDATA *pClient)
{
LogRelFlow(("vboxClipboardDisconnect\n"));
LogRel(("Stopping the host clipboard service\n"));
VBOXCLIPBOARDCONTEXT *pCtx = pClient->pCtx;
/* Drop the reference to the client, in case it is still there. This
* will cause any outstanding clipboard data requests from X11 to fail
* immediately. */
pCtx->fShuttingDown = true;
/* If there is a currently pending request, release it immediately. */
vboxClipboardWriteData(pClient, NULL, 0, 0);
int rc = ClipStopX11(pCtx->pBackend);
/** @todo handle this slightly more reasonably, or be really sure
* it won't go wrong. */
AssertRC(rc);
if (RT_SUCCESS(rc)) /* And if not? */
{
ClipDestructX11(pCtx->pBackend);
RTCritSectDelete(&pCtx->clipboardMutex);
RTMemFree(pCtx);
}
}
/**
* VBox is taking possession of the shared clipboard.
*
* @param pClient Context data for the guest system
* @param u32Formats Clipboard formats the guest is offering
* @note Host glue code
*/
void vboxClipboardFormatAnnounce (VBOXCLIPBOARDCLIENTDATA *pClient,
uint32_t u32Formats)
{
LogRelFlowFunc(("called. pClient=%p, u32Formats=%02X\n", pClient,
u32Formats));
ClipAnnounceFormatToX11 (pClient->pCtx->pBackend, u32Formats);
}
/** Structure describing a request for clipoard data from the guest. */
struct _CLIPREADCBREQ
{
/** Where to write the returned data to. */
void *pv;
/** The size of the buffer in pv */
uint32_t cb;
/** The actual size of the data written */
uint32_t *pcbActual;
};
/**
* Called when VBox wants to read the X11 clipboard.
*
* @returns VINF_SUCCESS on successful completion
* @returns VINF_HGCM_ASYNC_EXECUTE if the operation will complete
* asynchronously
* @returns iprt status code on failure
* @param pClient Context information about the guest VM
* @param u32Format The format that the guest would like to receive the data in
* @param pv Where to write the data to
* @param cb The size of the buffer to write the data to
* @param pcbActual Where to write the actual size of the written data
* @note We always fail or complete asynchronously
* @note On success allocates a CLIPREADCBREQ structure which must be
* freed in ClipCompleteDataRequestFromX11 when it is called back from
* the backend code.
*
*/
int vboxClipboardReadData (VBOXCLIPBOARDCLIENTDATA *pClient,
uint32_t u32Format, void *pv, uint32_t cb,
uint32_t *pcbActual)
{
LogRelFlowFunc(("pClient=%p, u32Format=%02X, pv=%p, cb=%u, pcbActual=%p",
pClient, u32Format, pv, cb, pcbActual));
int rc = VINF_SUCCESS;
CLIPREADCBREQ *pReq = (CLIPREADCBREQ *) RTMemAlloc(sizeof(CLIPREADCBREQ));
if (!pReq)
rc = VERR_NO_MEMORY;
else
{
pReq->pv = pv;
pReq->cb = cb;
pReq->pcbActual = pcbActual;
rc = ClipRequestDataFromX11(pClient->pCtx->pBackend, u32Format, pReq);
if (RT_SUCCESS(rc))
rc = VINF_HGCM_ASYNC_EXECUTE;
}
LogRelFlowFunc(("returning %Rrc\n", rc));
return rc;
}
/**
* Complete a request from VBox for the X11 clipboard data. The data should
* be written to the buffer provided in the initial request.
* @param pCtx request context information
* @param rc the completion status of the request
* @param pReq request
* @param pv address
* @param cb size
*
* @todo change this to deal with the buffer issues rather than offloading
* them onto the caller
*/
void ClipCompleteDataRequestFromX11(VBOXCLIPBOARDCONTEXT *pCtx, int rc,
CLIPREADCBREQ *pReq, void *pv, uint32_t cb)
{
if (cb <= pReq->cb && cb != 0)
memcpy(pReq->pv, pv, cb);
RTMemFree(pReq);
vboxSvcClipboardCompleteReadData(pCtx->pClient, rc, cb);
}
/** A request for clipboard data from VBox */
struct _VBOXCLIPBOARDREQFROMVBOX
{
/** Data received */
void *pv;
/** The size of the data */
uint32_t cb;
/** Format of the data */
uint32_t format;
/** A semaphore for waiting for the data */
RTSEMEVENT finished;
};
/** Wait for clipboard data requested from VBox to arrive. */
static int clipWaitForDataFromVBox(VBOXCLIPBOARDCONTEXT *pCtx,
VBOXCLIPBOARDREQFROMVBOX *pReq,
uint32_t u32Format)
{
int rc = VINF_SUCCESS;
LogRelFlowFunc(("pCtx=%p, pReq=%p, u32Format=%02X\n", pCtx, pReq, u32Format));
/* Request data from VBox */
vboxSvcClipboardReportMsg(pCtx->pClient,
VBOX_SHARED_CLIPBOARD_HOST_MSG_READ_DATA,
u32Format);
/* Which will signal us when it is ready. We use a timeout here
* because we can't be sure that the guest will behave correctly.
*/
rc = RTSemEventWait(pReq->finished, CLIPBOARD_TIMEOUT);
/* If the request hasn't yet completed then we cancel it. We use
* the critical section to prevent these operations colliding. */
RTCritSectEnter(&pCtx->clipboardMutex);
/* The data may have arrived between the semaphore timing out and
* our grabbing the mutex. */
if (rc == VERR_TIMEOUT && pReq->pv != NULL)
rc = VINF_SUCCESS;
if (pCtx->pReq == pReq)
pCtx->pReq = NULL;
Assert(pCtx->pReq == NULL);
RTCritSectLeave(&pCtx->clipboardMutex);
if (RT_SUCCESS(rc) && (pReq->pv == NULL))
rc = VERR_NO_DATA;
LogRelFlowFunc(("returning %Rrc\n", rc));
return rc;
}
/** Post a request for clipboard data to VBox/the guest and wait for it to be
* completed. */
static int clipRequestDataFromVBox(VBOXCLIPBOARDCONTEXT *pCtx,
VBOXCLIPBOARDREQFROMVBOX *pReq,
uint32_t u32Format)
{
int rc = VINF_SUCCESS;
LogRelFlowFunc(("pCtx=%p, pReq=%p, u32Format=%02X\n", pCtx, pReq,
u32Format));
/* Start by "posting" the request for the next invocation of
* vboxClipboardWriteData. */
RTCritSectEnter(&pCtx->clipboardMutex);
if (pCtx->pReq != NULL)
{
/* This would be a violation of the protocol, see the comments in the
* context structure definition. */
Assert(false);
rc = VERR_WRONG_ORDER;
}
else
pCtx->pReq = pReq;
RTCritSectLeave(&pCtx->clipboardMutex);
if (RT_SUCCESS(rc))
rc = clipWaitForDataFromVBox(pCtx, pReq, u32Format);
LogRelFlowFunc(("returning %Rrc\n", rc));
return rc;
}
/**
* Send a request to VBox to transfer the contents of its clipboard to X11.
*
* @param pCtx Pointer to the host clipboard structure
* @param u32Format The format in which the data should be transferred
* @param ppv On success and if pcb > 0, this will point to a buffer
* to be freed with RTMemFree containing the data read.
* @param pcb On success, this contains the number of bytes of data
* returned
* @note Host glue code.
*/
int ClipRequestDataForX11 (VBOXCLIPBOARDCONTEXT *pCtx,
uint32_t u32Format, void **ppv,
uint32_t *pcb)
{
VBOXCLIPBOARDREQFROMVBOX request = { NULL, 0, 0, NIL_RTSEMEVENT };
LogRelFlowFunc(("pCtx=%p, u32Format=%02X, ppv=%p, pcb=%p\n", pCtx,
u32Format, ppv, pcb));
if (pCtx->fShuttingDown)
{
/* The shared clipboard is disconnecting. */
LogRelFunc(("host requested guest clipboard data after guest had disconnected.\n"));
return VERR_WRONG_ORDER;
}
int rc = RTSemEventCreate(&request.finished);
if (RT_SUCCESS(rc))
{
rc = clipRequestDataFromVBox(pCtx, &request, u32Format);
RTSemEventDestroy(request.finished);
}
if (RT_SUCCESS(rc))
{
*ppv = request.pv;
*pcb = request.cb;
}
LogRelFlowFunc(("returning %Rrc\n", rc));
if (RT_SUCCESS(rc))
LogRelFlowFunc(("*ppv=%.*ls, *pcb=%u\n", *pcb / 2, *ppv, *pcb));
return rc;
}
/**
* Called when we have requested data from VBox and that data has arrived.
*
* @param pClient Context information about the guest VM
* @param pv Buffer to which the data was written
* @param cb The size of the data written
* @param u32Format The format of the data written
* @note Host glue code
*/
void vboxClipboardWriteData (VBOXCLIPBOARDCLIENTDATA *pClient,
void *pv, uint32_t cb, uint32_t u32Format)
{
LogRelFlowFunc (("called. pClient=%p, pv=%p (%.*ls), cb=%u, u32Format=%02X\n",
pClient, pv, cb / 2, pv, cb, u32Format));
VBOXCLIPBOARDCONTEXT *pCtx = pClient->pCtx;
/* Grab the mutex and check whether there is a pending request for data.
*/
RTCritSectEnter(&pCtx->clipboardMutex);
VBOXCLIPBOARDREQFROMVBOX *pReq = pCtx->pReq;
if (pReq != NULL)
{
if (cb > 0)
{
pReq->pv = RTMemDup(pv, cb);
if (pReq->pv != NULL) /* NULL may also mean no memory... */
{
pReq->cb = cb;
pReq->format = u32Format;
}
}
/* Signal that the request has been completed. */
RTSemEventSignal(pReq->finished);
pCtx->pReq = NULL;
}
RTCritSectLeave(&pCtx->clipboardMutex);
}
#ifdef TESTCASE
#include <iprt/initterm.h>
#include <iprt/stream.h>
#define TEST_NAME "tstClipboardX11-2"
struct _CLIPBACKEND
{
uint32_t formats;
struct _READDATA
{
uint32_t format;
int rc;
CLIPREADCBREQ *pReq;
} readData;
struct _COMPLETEREAD
{
int rc;
uint32_t cbActual;
} completeRead;
struct _WRITEDATA
{
void *pv;
uint32_t cb;
uint32_t format;
bool timeout;
} writeData;
struct _REPORTDATA
{
uint32_t format;
} reportData;
};
void vboxSvcClipboardReportMsg (VBOXCLIPBOARDCLIENTDATA *pClient, uint32_t u32Msg, uint32_t u32Formats)
{
RT_NOREF1(u32Formats);
CLIPBACKEND *pBackend = pClient->pCtx->pBackend;
if ( (u32Msg == VBOX_SHARED_CLIPBOARD_HOST_MSG_READ_DATA)
&& !pBackend->writeData.timeout)
vboxClipboardWriteData(pClient, pBackend->writeData.pv,
pBackend->writeData.cb,
pBackend->writeData.format);
else
return;
}
void vboxSvcClipboardCompleteReadData(VBOXCLIPBOARDCLIENTDATA *pClient, int rc, uint32_t cbActual)
{
CLIPBACKEND *pBackend = pClient->pCtx->pBackend;
pBackend->completeRead.rc = rc;
pBackend->completeRead.cbActual = cbActual;
}
CLIPBACKEND *ClipConstructX11(VBOXCLIPBOARDCONTEXT *pFrontend, bool)
{
RT_NOREF1(pFrontend);
return (CLIPBACKEND *)RTMemAllocZ(sizeof(CLIPBACKEND));
}
void ClipDestructX11(CLIPBACKEND *pBackend)
{
RTMemFree(pBackend);
}
int ClipStartX11(CLIPBACKEND *pBackend, bool)
{
RT_NOREF1(pBackend);
return VINF_SUCCESS;
}
int ClipStopX11(CLIPBACKEND *pBackend)
{
RT_NOREF1(pBackend);
return VINF_SUCCESS;
}
void ClipAnnounceFormatToX11(CLIPBACKEND *pBackend,
uint32_t u32Formats)
{
pBackend->formats = u32Formats;
}
extern int ClipRequestDataFromX11(CLIPBACKEND *pBackend, uint32_t u32Format,
CLIPREADCBREQ *pReq)
{
pBackend->readData.format = u32Format;
pBackend->readData.pReq = pReq;
return pBackend->readData.rc;
}
int main()
{
VBOXCLIPBOARDCLIENTDATA client;
unsigned cErrors = 0;
int rc = RTR3InitExeNoArguments(0);
RTPrintf(TEST_NAME ": TESTING\n");
AssertRCReturn(rc, 1);
rc = vboxClipboardConnect(&client, false);
CLIPBACKEND *pBackend = client.pCtx->pBackend;
AssertRCReturn(rc, 1);
vboxClipboardFormatAnnounce(&client,
VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT);
if (pBackend->formats != VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT)
{
RTPrintf(TEST_NAME ": vboxClipboardFormatAnnounce failed with VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT\n");
++cErrors;
}
pBackend->readData.rc = VINF_SUCCESS;
client.asyncRead.callHandle = (VBOXHGCMCALLHANDLE)pBackend;
client.asyncRead.paParms = (VBOXHGCMSVCPARM *)&client;
uint32_t u32Dummy;
rc = vboxClipboardReadData(&client, VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT,
&u32Dummy, 42, &u32Dummy);
if (rc != VINF_HGCM_ASYNC_EXECUTE)
{
RTPrintf(TEST_NAME ": vboxClipboardReadData returned %Rrc\n", rc);
++cErrors;
}
else
{
if ( pBackend->readData.format !=
VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT
|| pBackend->readData.pReq->pv != &u32Dummy
|| pBackend->readData.pReq->cb != 42
|| pBackend->readData.pReq->pcbActual != &u32Dummy)
{
RTPrintf(TEST_NAME ": format=%u, pReq->pv=%p, pReq->cb=%u, pReq->pcbActual=%p\n",
pBackend->readData.format, pBackend->readData.pReq->pv,
pBackend->readData.pReq->cb,
pBackend->readData.pReq->pcbActual);
++cErrors;
}
else
{
ClipCompleteDataRequestFromX11(client.pCtx, VERR_NO_DATA,
pBackend->readData.pReq, NULL, 43);
if ( pBackend->completeRead.rc != VERR_NO_DATA
|| pBackend->completeRead.cbActual != 43)
{
RTPrintf(TEST_NAME ": rc=%Rrc, cbActual=%u\n",
pBackend->completeRead.rc,
pBackend->completeRead.cbActual);
++cErrors;
}
}
}
void *pv;
uint32_t cb;
pBackend->writeData.pv = (void *)"testing";
pBackend->writeData.cb = sizeof("testing");
pBackend->writeData.format = 1234;
pBackend->reportData.format = 4321; /* XX this should be handled! */
rc = ClipRequestDataForX11(client.pCtx, 23, &pv, &cb);
if ( rc != VINF_SUCCESS
|| strcmp((const char *)pv, "testing") != 0
|| cb != sizeof("testing"))
{
RTPrintf("rc=%Rrc, pv=%p, cb=%u\n", rc, pv, cb);
++cErrors;
}
else
RTMemFree(pv);
pBackend->writeData.timeout = true;
rc = ClipRequestDataForX11(client.pCtx, 23, &pv, &cb);
if (rc != VERR_TIMEOUT)
{
RTPrintf("rc=%Rrc, expected VERR_TIMEOUT\n", rc);
++cErrors;
}
pBackend->writeData.pv = NULL;
pBackend->writeData.cb = 0;
pBackend->writeData.timeout = false;
rc = ClipRequestDataForX11(client.pCtx, 23, &pv, &cb);
if (rc != VERR_NO_DATA)
{
RTPrintf("rc=%Rrc, expected VERR_NO_DATA\n", rc);
++cErrors;
}
/* Data arriving after a timeout should *not* cause any segfaults or
* memory leaks. Check with Valgrind! */
vboxClipboardWriteData(&client, (void *)"tested", sizeof("tested"), 999);
vboxClipboardDisconnect(&client);
if (cErrors > 0)
RTPrintf(TEST_NAME ": errors: %u\n", cErrors);
return cErrors > 0 ? 1 : 0;
}
#endif /* TESTCASE */
|