1 /*
2 *
3 * Copyright (C) 1994-2018, OFFIS e.V.
4 * All rights reserved. See COPYRIGHT file for details.
5 *
6 * This software and supporting documentation were developed by
7 *
8 * OFFIS e.V.
9 * R&D Division Health
10 * Escherweg 2
11 * D-26121 Oldenburg, Germany
12 *
13 *
14 * Module: dcmnet
15 *
16 * Author: Andrew Hewett
17 *
18 * Purpose: Verification Service Class User (C-ECHO operation)
19 *
20 */
21
22 #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */
23
24 #define INCLUDE_CSTDLIB
25 #define INCLUDE_CSTDIO
26 #define INCLUDE_CSTRING
27 #define INCLUDE_CSTDARG
28 #include "dcmtk/ofstd/ofstdinc.h"
29
30 #include "dcmtk/dcmnet/dimse.h"
31 #include "dcmtk/dcmnet/diutil.h"
32 #include "dcmtk/dcmnet/dcmtrans.h" /* for dcmSocketSend/ReceiveTimeout */
33 #include "dcmtk/dcmdata/dcfilefo.h"
34 #include "dcmtk/dcmdata/dcdict.h"
35 #include "dcmtk/dcmdata/dcuid.h"
36 #include "dcmtk/dcmdata/cmdlnarg.h"
37 #include "dcmtk/ofstd/ofconapp.h"
38 #include "dcmtk/dcmdata/dcuid.h" /* for dcmtk version name */
39 #include "dcmtk/dcmtls/tlsopt.h" /* for DcmTLSOptions */
40
41 #ifdef WITH_ZLIB
42 #include <zlib.h> /* for zlibVersion() */
43 #endif
44
45 #ifdef PRIVATE_ECHOSCU_DECLARATIONS
46 PRIVATE_ECHOSCU_DECLARATIONS
47 #else
48 #define OFFIS_CONSOLE_APPLICATION "echoscu"
49 #endif
50
51 static OFLogger echoscuLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION);
52
53 static char rcsid[] = "$dcmtk: " OFFIS_CONSOLE_APPLICATION " v"
54 OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $";
55
56 /* default application titles */
57 #define APPLICATIONTITLE "ECHOSCU"
58 #define PEERAPPLICATIONTITLE "ANY-SCP"
59
60
61 /* exit codes for this command line tool */
62 /* (common codes are defined in "ofexit.h" included from "ofconapp.h") */
63 // network errors
64 #define EXITCODE_ASSOCIATION_ABORTED 70
65
66 static T_DIMSE_BlockingMode opt_blockMode = DIMSE_BLOCKING;
67 static int opt_dimse_timeout = 0;
68
69 static OFCondition cecho(T_ASC_Association * assoc, unsigned long num_repeat);
70
71 /* DICOM standard transfer syntaxes */
72 static const char* transferSyntaxes[] = {
73 UID_LittleEndianImplicitTransferSyntax, /* default xfer syntax first */
74 UID_LittleEndianExplicitTransferSyntax,
75 UID_BigEndianExplicitTransferSyntax,
76 UID_JPEGProcess1TransferSyntax,
77 UID_JPEGProcess2_4TransferSyntax,
78 UID_JPEGProcess3_5TransferSyntax,
79 UID_JPEGProcess6_8TransferSyntax,
80 UID_JPEGProcess7_9TransferSyntax,
81 UID_JPEGProcess10_12TransferSyntax,
82 UID_JPEGProcess11_13TransferSyntax,
83 UID_JPEGProcess14TransferSyntax,
84 UID_JPEGProcess15TransferSyntax,
85 UID_JPEGProcess16_18TransferSyntax,
86 UID_JPEGProcess17_19TransferSyntax,
87 UID_JPEGProcess20_22TransferSyntax,
88 UID_JPEGProcess21_23TransferSyntax,
89 UID_JPEGProcess24_26TransferSyntax,
90 UID_JPEGProcess25_27TransferSyntax,
91 UID_JPEGProcess28TransferSyntax,
92 UID_JPEGProcess29TransferSyntax,
93 UID_JPEGProcess14SV1TransferSyntax,
94 UID_RLELosslessTransferSyntax,
95 UID_DeflatedExplicitVRLittleEndianTransferSyntax,
96 UID_JPEGLSLosslessTransferSyntax,
97 UID_JPEGLSLossyTransferSyntax,
98 UID_JPEG2000LosslessOnlyTransferSyntax,
99 UID_JPEG2000TransferSyntax,
100 UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax,
101 UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax,
102 UID_MPEG2MainProfileAtMainLevelTransferSyntax,
103 UID_MPEG2MainProfileAtHighLevelTransferSyntax,
104 UID_MPEG4HighProfileLevel4_1TransferSyntax,
105 UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax,
106 UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax,
107 UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax,
108 UID_MPEG4StereoHighProfileLevel4_2TransferSyntax,
109 UID_HEVCMainProfileLevel5_1TransferSyntax,
110 UID_HEVCMain10ProfileLevel5_1TransferSyntax
111 };
112
113 // ********************************************
114
115 /* helper macro for converting stream output to a string */
116 #define CONVERT_TO_STRING(output, string) \
117 optStream.str(""); \
118 optStream.clear(); \
119 optStream << output << OFStringStream_ends; \
120 OFSTRINGSTREAM_GETOFSTRING(optStream, string)
121
122 #define SHORTCOL 4
123 #define LONGCOL 19
124
125 int
main(int argc,char * argv[])126 main(int argc, char *argv[])
127 {
128 OFOStringStream optStream;
129 int result = EXITCODE_NO_ERROR;
130
131 const char * opt_peer = NULL;
132 OFCmdUnsignedInt opt_port = 104;
133 const char * opt_peerTitle = PEERAPPLICATIONTITLE;
134 const char * opt_ourTitle = APPLICATIONTITLE;
135 OFCmdUnsignedInt opt_maxReceivePDULength = ASC_DEFAULTMAXPDU;
136 OFCmdUnsignedInt opt_repeatCount = 1;
137 OFBool opt_abortAssociation = OFFalse;
138 OFCmdUnsignedInt opt_numXferSyntaxes = 1;
139 OFCmdUnsignedInt opt_numPresentationCtx = 1;
140 OFCmdUnsignedInt maxXferSyntaxes = OFstatic_cast(OFCmdUnsignedInt, (DIM_OF(transferSyntaxes)));
141 int opt_acse_timeout = 30;
142 OFCmdSignedInt opt_socket_timeout = 60;
143 DcmTLSOptions tlsOptions(NET_REQUESTOR);
144
145 T_ASC_Network *net;
146 T_ASC_Parameters *params;
147 DIC_NODENAME peerHost;
148 T_ASC_Association *assoc;
149 OFString temp_str;
150
151 OFStandard::initializeNetwork();
152 #ifdef WITH_OPENSSL
153 DcmTLSTransportLayer::initializeOpenSSL();
154 #endif
155
156 char tempstr[20];
157 OFConsoleApplication app(OFFIS_CONSOLE_APPLICATION , "DICOM verification (C-ECHO) SCU", rcsid);
158 OFCommandLine cmd;
159
160 cmd.setParamColumn(LONGCOL + SHORTCOL + 4);
161 cmd.addParam("peer", "hostname of DICOM peer");
162 cmd.addParam("port", "tcp/ip port number of peer");
163
164 cmd.setOptionColumns(LONGCOL, SHORTCOL);
165 cmd.addGroup("general options:", LONGCOL, SHORTCOL + 2);
166 cmd.addOption("--help", "-h", "print this help text and exit", OFCommandLine::AF_Exclusive);
167 cmd.addOption("--version", "print version information and exit", OFCommandLine::AF_Exclusive);
168 OFLog::addOptions(cmd);
169
170 cmd.addGroup("network options:");
171 cmd.addSubGroup("application entity titles:");
172 cmd.addOption("--aetitle", "-aet", 1, "[a]etitle: string", "set my calling AE title (default: " APPLICATIONTITLE ")");
173 cmd.addOption("--call", "-aec", 1, "[a]etitle: string", "set called AE title of peer (default: " PEERAPPLICATIONTITLE ")");
174 cmd.addSubGroup("association negotiation debugging:");
175 OFString opt5 = "[n]umber: integer (1..";
176 sprintf(tempstr, "%ld", OFstatic_cast(long, maxXferSyntaxes));
177 opt5 += tempstr;
178 opt5 += ")";
179 cmd.addOption("--propose-ts", "-pts", 1, opt5.c_str(), "propose n transfer syntaxes");
180 cmd.addOption("--propose-pc", "-ppc", 1, "[n]umber: integer (1..128)", "propose n presentation contexts");
181
182 cmd.addSubGroup("other network options:");
183 cmd.addOption("--timeout", "-to", 1, "[s]econds: integer (default: unlimited)", "timeout for connection requests");
184 CONVERT_TO_STRING("[s]econds: integer (default: " << opt_socket_timeout << ")", optString1);
185 cmd.addOption("--socket-timeout", "-ts", 1, optString1.c_str(), "timeout for network socket (0 for none)");
186 CONVERT_TO_STRING("[s]econds: integer (default: " << opt_acse_timeout << ")", optString2);
187 cmd.addOption("--acse-timeout", "-ta", 1, optString2.c_str(), "timeout for ACSE messages");
188 cmd.addOption("--dimse-timeout", "-td", 1, "[s]econds: integer (default: unlimited)", "timeout for DIMSE messages");
189
190 CONVERT_TO_STRING("[n]umber of bytes: integer (" << ASC_MINIMUMPDUSIZE << ".." << ASC_MAXIMUMPDUSIZE << ")", optString3);
191 CONVERT_TO_STRING("set max receive pdu to n bytes (default: " << opt_maxReceivePDULength << ")", optString4);
192 cmd.addOption("--max-pdu", "-pdu", 1, optString3.c_str(), optString4.c_str());
193 cmd.addOption("--repeat", 1, "[n]umber: integer", "repeat n times");
194 cmd.addOption("--abort", "abort association instead of releasing it");
195
196 // add TLS specific command line options if (and only if) we are compiling with OpenSSL
197 tlsOptions.addTLSCommandlineOptions(cmd);
198
199 /* evaluate command line */
200 prepareCmdLineArgs(argc, argv, OFFIS_CONSOLE_APPLICATION);
201 if (app.parseCommandLine(cmd, argc, argv))
202 {
203 /* check exclusive options first */
204 if (cmd.hasExclusiveOption())
205 {
206 if (cmd.findOption("--version"))
207 {
208 app.printHeader(OFTrue /*print host identifier*/);
209 COUT << OFendl << "External libraries used:";
210 #if !defined(WITH_ZLIB) && !defined(WITH_OPENSSL)
211 COUT << " none" << OFendl;
212 #else
213 COUT << OFendl;
214 #endif
215 #ifdef WITH_ZLIB
216 COUT << "- ZLIB, Version " << zlibVersion() << OFendl;
217 #endif
218 // print OpenSSL version if (and only if) we are compiling with OpenSSL
219 tlsOptions.printLibraryVersion();
220 return EXITCODE_NO_ERROR;
221 }
222
223 // check if the command line contains the --list-ciphers option
224 if (tlsOptions.listOfCiphersRequested(cmd))
225 {
226 tlsOptions.printSupportedCiphersuites(app, COUT);
227 return EXITCODE_NO_ERROR;
228 }
229 }
230
231 /* command line parameters */
232
233 cmd.getParam(1, opt_peer);
234 app.checkParam(cmd.getParamAndCheckMinMax(2, opt_port, 1, 65535));
235
236 OFLog::configureFromCommandLine(cmd, app);
237
238 if (cmd.findOption("--aetitle")) app.checkValue(cmd.getValue(opt_ourTitle));
239 if (cmd.findOption("--call")) app.checkValue(cmd.getValue(opt_peerTitle));
240
241 if (cmd.findOption("--timeout"))
242 {
243 OFCmdSignedInt opt_timeout = 0;
244 app.checkValue(cmd.getValueAndCheckMin(opt_timeout, 1));
245 dcmConnectionTimeout.set(OFstatic_cast(Sint32, opt_timeout));
246 }
247
248 if (cmd.findOption("--socket-timeout"))
249 app.checkValue(cmd.getValueAndCheckMin(opt_socket_timeout, -1));
250 // always set the timeout values since the global default might be different
251 dcmSocketSendTimeout.set(OFstatic_cast(Sint32, opt_socket_timeout));
252 dcmSocketReceiveTimeout.set(OFstatic_cast(Sint32, opt_socket_timeout));
253
254 if (cmd.findOption("--acse-timeout"))
255 {
256 OFCmdSignedInt opt_timeout = 0;
257 app.checkValue(cmd.getValueAndCheckMin(opt_timeout, 1));
258 opt_acse_timeout = OFstatic_cast(int, opt_timeout);
259 }
260
261 if (cmd.findOption("--dimse-timeout"))
262 {
263 OFCmdSignedInt opt_timeout = 0;
264 app.checkValue(cmd.getValueAndCheckMin(opt_timeout, 1));
265 opt_dimse_timeout = OFstatic_cast(int, opt_timeout);
266 opt_blockMode = DIMSE_NONBLOCKING;
267 }
268
269 if (cmd.findOption("--max-pdu")) app.checkValue(cmd.getValueAndCheckMinMax(opt_maxReceivePDULength, ASC_MINIMUMPDUSIZE, ASC_MAXIMUMPDUSIZE));
270 if (cmd.findOption("--repeat")) app.checkValue(cmd.getValueAndCheckMin(opt_repeatCount, 1));
271 if (cmd.findOption("--abort")) opt_abortAssociation=OFTrue;
272 if (cmd.findOption("--propose-ts")) app.checkValue(cmd.getValueAndCheckMinMax(opt_numXferSyntaxes, 1, maxXferSyntaxes));
273 if (cmd.findOption("--propose-pc")) app.checkValue(cmd.getValueAndCheckMinMax(opt_numPresentationCtx, 1, 128));
274
275 // evaluate (most of) the TLS command line options (if we are compiling with OpenSSL)
276 tlsOptions.parseArguments(app, cmd);
277
278 }
279
280 /* print resource identifier */
281 OFLOG_DEBUG(echoscuLogger, rcsid << OFendl);
282
283 /* make sure data dictionary is loaded */
284 if (!dcmDataDict.isDictionaryLoaded())
285 {
286 OFLOG_WARN(echoscuLogger, "no data dictionary loaded, check environment variable: "
287 << DCM_DICT_ENVIRONMENT_VARIABLE);
288 }
289
290 /* initialize network, i.e. create an instance of T_ASC_Network*. */
291 OFCondition cond = ASC_initializeNetwork(NET_REQUESTOR, 0, opt_acse_timeout, &net);
292 if (cond.bad()) {
293 OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
294 exit(1);
295 }
296
297 /* initialize association parameters, i.e. create an instance of T_ASC_Parameters*. */
298 cond = ASC_createAssociationParameters(¶ms, opt_maxReceivePDULength);
299 if (cond.bad()) {
300 OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
301 exit(1);
302 }
303
304 /* create a secure transport layer if requested and OpenSSL is available */
305 cond = tlsOptions.createTransportLayer(net, params, app, cmd);
306 if (cond.bad()) {
307 OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
308 exit(1);
309 }
310
311 #ifdef PRIVATE_ECHOSCU_CODE
312 PRIVATE_ECHOSCU_CODE
313 #endif
314
315 /* sets this application's title and the called application's title in the params */
316 /* structure. The default values to be set here are "STORESCU" and "ANY-SCP". */
317 ASC_setAPTitles(params, opt_ourTitle, opt_peerTitle, NULL);
318
319 /* Figure out the presentation addresses and copy the */
320 /* corresponding values into the association parameters.*/
321 sprintf(peerHost, "%s:%d", opt_peer, OFstatic_cast(int, opt_port));
322 ASC_setPresentationAddresses(params, OFStandard::getHostName().c_str(), peerHost);
323
324 /* Set the presentation contexts which will be negotiated */
325 /* when the network connection will be established */
326 int presentationContextID = 1; /* odd byte value 1, 3, 5, .. 255 */
327 for (unsigned long ii=0; ii<opt_numPresentationCtx; ii++)
328 {
329 cond = ASC_addPresentationContext(params, presentationContextID, UID_VerificationSOPClass,
330 transferSyntaxes, OFstatic_cast(int, opt_numXferSyntaxes));
331 presentationContextID += 2;
332 if (cond.bad())
333 {
334 OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
335 exit(1);
336 }
337 }
338
339 /* dump presentation contexts if required */
340 OFLOG_DEBUG(echoscuLogger, "Request Parameters:" << OFendl << ASC_dumpParameters(temp_str, params, ASC_ASSOC_RQ));
341
342 /* create association, i.e. try to establish a network connection to another */
343 /* DICOM application. This call creates an instance of T_ASC_Association*. */
344 OFLOG_INFO(echoscuLogger, "Requesting Association");
345 cond = ASC_requestAssociation(net, params, &assoc);
346 if (cond.bad()) {
347 if (cond == DUL_ASSOCIATIONREJECTED)
348 {
349 T_ASC_RejectParameters rej;
350
351 ASC_getRejectParameters(params, &rej);
352 OFLOG_FATAL(echoscuLogger, "Association Rejected:" << OFendl << ASC_printRejectParameters(temp_str, &rej));
353 exit(1);
354 } else {
355 OFLOG_FATAL(echoscuLogger, "Association Request Failed: " << DimseCondition::dump(temp_str, cond));
356 exit(1);
357 }
358 }
359
360 /* dump the presentation contexts which have been accepted/refused */
361 OFLOG_DEBUG(echoscuLogger, "Association Parameters Negotiated:" << OFendl << ASC_dumpParameters(temp_str, params, ASC_ASSOC_AC));
362
363 /* count the presentation contexts which have been accepted by the SCP */
364 /* If there are none, finish the execution */
365 if (ASC_countAcceptedPresentationContexts(params) == 0) {
366 OFLOG_FATAL(echoscuLogger, "No Acceptable Presentation Contexts");
367 exit(1);
368 }
369
370 /* dump general information concerning the establishment of the network connection if required */
371 OFLOG_INFO(echoscuLogger, "Association Accepted (Max Send PDV: " << assoc->sendPDVLength << ")");
372
373 /* do the real work, i.e. send a number of C-ECHO-RQ messages to the DICOM application */
374 /* this application is connected with and handle corresponding C-ECHO-RSP messages. */
375 cond = cecho(assoc, opt_repeatCount);
376
377 /* tear down association, i.e. terminate network connection to SCP */
378 if (cond == EC_Normal)
379 {
380 if (opt_abortAssociation) {
381 OFLOG_INFO(echoscuLogger, "Aborting Association");
382 cond = ASC_abortAssociation(assoc);
383 if (cond.bad())
384 {
385 OFLOG_FATAL(echoscuLogger, "Association Abort Failed: " << DimseCondition::dump(temp_str, cond));
386 exit(1);
387 }
388 } else {
389 /* release association */
390 OFLOG_INFO(echoscuLogger, "Releasing Association");
391 cond = ASC_releaseAssociation(assoc);
392 if (cond.bad())
393 {
394 OFLOG_FATAL(echoscuLogger, "Association Release Failed: " << DimseCondition::dump(temp_str, cond));
395 exit(1);
396 }
397 }
398 }
399 else if (cond == DUL_PEERREQUESTEDRELEASE)
400 {
401 OFLOG_FATAL(echoscuLogger, "Protocol Error: Peer requested release (Aborting)");
402 OFLOG_INFO(echoscuLogger, "Aborting Association");
403 cond = ASC_abortAssociation(assoc);
404 result = EXITCODE_ASSOCIATION_ABORTED;// return an error code at the end of main
405 if (cond.bad()) {
406 OFLOG_FATAL(echoscuLogger, "Association Abort Failed: " << DimseCondition::dump(temp_str, cond));
407 exit(1);
408 }
409 }
410 else if (cond == DUL_PEERABORTEDASSOCIATION)
411 {
412 OFLOG_INFO(echoscuLogger, "Peer Aborted Association");
413 }
414 else
415 {
416 OFLOG_ERROR(echoscuLogger, "Echo SCU Failed: " << DimseCondition::dump(temp_str, cond));
417 OFLOG_INFO(echoscuLogger, "Aborting Association");
418 cond = ASC_abortAssociation(assoc);
419 result = EXITCODE_ASSOCIATION_ABORTED; // return an error code at the end of main
420 if (cond.bad()) {
421 OFLOG_FATAL(echoscuLogger, "Association Abort Failed: " << DimseCondition::dump(temp_str, cond));
422 exit(1);
423 }
424 }
425
426 /* destroy the association, i.e. free memory of T_ASC_Association* structure. This */
427 /* call is the counterpart of ASC_requestAssociation(...) which was called above. */
428 cond = ASC_destroyAssociation(&assoc);
429 if (cond.bad()) {
430 OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
431 exit(1);
432 }
433
434 /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
435 /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
436 cond = ASC_dropNetwork(&net);
437 if (cond.bad()) {
438 OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
439 exit(1);
440 }
441
442 OFStandard::shutdownNetwork();
443
444 cond = tlsOptions.writeRandomSeed();
445 if (cond.bad()) {
446 // failure to write back the random seed is a warning, not an error
447 OFLOG_WARN(echoscuLogger, DimseCondition::dump(temp_str, cond));
448 }
449
450 return result;
451 }
452
453 static OFCondition
echoSCU(T_ASC_Association * assoc)454 echoSCU(T_ASC_Association * assoc)
455 /*
456 * This function will send a C-ECHO-RQ over the network to another DICOM application
457 * and handle the response.
458 *
459 * Parameters:
460 * assoc - [in] The association (network connection to another DICOM application).
461 */
462 {
463 DIC_US msgId = assoc->nextMsgID++;
464 DIC_US status;
465 DcmDataset *statusDetail = NULL;
466
467 /* dump information if required */
468 OFLOG_INFO(echoscuLogger, "Sending Echo Request (MsgID " << msgId << ")");
469
470 /* send C-ECHO-RQ and handle response */
471 OFCondition cond = DIMSE_echoUser(assoc, msgId, opt_blockMode, opt_dimse_timeout, &status, &statusDetail);
472
473 /* depending on if a response was received, dump some information */
474 if (cond.good()) {
475 OFLOG_INFO(echoscuLogger, "Received Echo Response (" << DU_cechoStatusString(status) << ")");
476 } else {
477 OFString temp_str;
478 OFLOG_ERROR(echoscuLogger, "Echo Failed: " << DimseCondition::dump(temp_str, cond));
479 }
480
481 /* check for status detail information, there should never be any */
482 if (statusDetail != NULL) {
483 OFLOG_DEBUG(echoscuLogger, "Status Detail (should never be any):" << OFendl << DcmObject::PrintHelper(*statusDetail));
484 delete statusDetail;
485 }
486
487 /* return result value */
488 return cond;
489 }
490
491 static OFCondition
cecho(T_ASC_Association * assoc,unsigned long num_repeat)492 cecho(T_ASC_Association * assoc, unsigned long num_repeat)
493 /*
494 * This function will send num_repeat C-ECHO-RQ messages to the DICOM application
495 * this application is connected with and handle corresponding C-ECHO-RSP messages.
496 *
497 * Parameters:
498 * assoc - [in] The association (network connection to another DICOM application).
499 * num_repeat - [in] The amount of C-ECHO-RQ messages which shall be sent.
500 */
501 {
502 OFCondition cond = EC_Normal;
503 unsigned long n = num_repeat;
504
505 /* as long as no error occurred and the counter does not equal 0 */
506 /* send an C-ECHO-RQ and handle the response */
507 while (cond.good() && n--) cond = echoSCU(assoc);
508
509 return cond;
510 }
511