1 | /**************************************************************** |
---|
2 | * |
---|
3 | * |
---|
4 | * MP version 1.1.2: Multi Protocol |
---|
5 | * Kent State University, Kent, OH |
---|
6 | * Authors: S. Gray, N. Kajler, P. Wang |
---|
7 | * (C) 1993, 1994, 1995, 1996, 1997 All Rights Reserved |
---|
8 | * |
---|
9 | * NOTICE |
---|
10 | * |
---|
11 | * Permission to use, copy, modify, and distribute this software and |
---|
12 | * its documentation for non-commercial purposes and without fee is |
---|
13 | * hereby granted provided that the above copyright notice appear in all |
---|
14 | * copies and that both the copyright notice and this permission notice |
---|
15 | * appear in supporting documentation. |
---|
16 | * |
---|
17 | * Neither Kent State University nor the Authors make any representations |
---|
18 | * about the suitability of this software for any purpose. The MP Library |
---|
19 | * is distributed in the hope that it will be useful, but is provided "as |
---|
20 | * is" and without any warranty of any kind and without even the implied |
---|
21 | * warranty of merchantability or fitness for a particular purpose. |
---|
22 | * |
---|
23 | * |
---|
24 | * IMPLEMENTATION FILE: MP_FileTransp.c |
---|
25 | * |
---|
26 | * Public interface routines to the "file" transport device. |
---|
27 | * These include read, write, open, close, and init routines. |
---|
28 | * There are some support routines as well. |
---|
29 | * |
---|
30 | * Change Log: |
---|
31 | * November 21, 1994 SG - Added file_has_data() and put it in the |
---|
32 | * the transp_op structure |
---|
33 | * September 11, 1995 SG - Reorganization of files. Cleaned up |
---|
34 | * header files and relocated source to |
---|
35 | * its own file. |
---|
36 | * 3/3/96 sgray - moved call to file_init_transport() inside |
---|
37 | * file_open_transport() and removed the init |
---|
38 | * routine from the file_ops struct. |
---|
39 | * 9/22/96 sgray - changed tv_sec to 0 in file_get_status() |
---|
40 | * so that select() does polling. |
---|
41 | * 3/11/97 sgray - Changed all memory management calls that allocate |
---|
42 | * or deallocate strings to use the "Raw" calls. |
---|
43 | * |
---|
44 | ***************************************************************************/ |
---|
45 | |
---|
46 | |
---|
47 | #ifndef lint |
---|
48 | static char vcid[] = "@(#) $Id: MP_FileTransp.c,v 1.2 1998-10-14 10:18:10 obachman Exp $"; |
---|
49 | #endif /* lint */ |
---|
50 | |
---|
51 | #include "MP.h" |
---|
52 | #include <sys/types.h> |
---|
53 | #include <fcntl.h> |
---|
54 | #include <sys/time.h> |
---|
55 | #include <sys/stat.h> |
---|
56 | #include <string.h> |
---|
57 | |
---|
58 | |
---|
59 | #ifdef __WIN32__ |
---|
60 | #include <io.h> |
---|
61 | #endif |
---|
62 | |
---|
63 | #ifdef __WIN32__ |
---|
64 | |
---|
65 | # include <sys/stat.h> |
---|
66 | # define FILE_MODE S_IREAD | S_IWRITE |
---|
67 | |
---|
68 | #else /* not __WIN32__ */ |
---|
69 | |
---|
70 | # define FILE_MODE 0666 |
---|
71 | # |
---|
72 | # ifndef O_BINARY |
---|
73 | # define O_BINARY 0 |
---|
74 | # endif /* O_BINARY */ |
---|
75 | |
---|
76 | #endif /* not __WIN32__ */ |
---|
77 | |
---|
78 | /* #define MP_DEBUG */ |
---|
79 | |
---|
80 | EXTERN char *IMP_GetCmdlineArg _ANSI_ARGS_((int, char**, char*)); |
---|
81 | |
---|
82 | #define log_msg_len 128 |
---|
83 | static char log_msg[log_msg_len]; |
---|
84 | |
---|
85 | |
---|
86 | MP_TranspOps_t file_ops = { |
---|
87 | file_write, |
---|
88 | file_read, |
---|
89 | file_flush, |
---|
90 | file_get_status, |
---|
91 | file_open_connection, |
---|
92 | file_close_connection, |
---|
93 | file_close_connection |
---|
94 | }; |
---|
95 | |
---|
96 | |
---|
97 | #ifdef __STDC__ |
---|
98 | int get_file_mode(int argc, |
---|
99 | char **argv) |
---|
100 | #else |
---|
101 | int get_file_mode(argc, argv) |
---|
102 | int argc; |
---|
103 | char **argv; |
---|
104 | #endif |
---|
105 | { |
---|
106 | char *cmode; |
---|
107 | |
---|
108 | if ((cmode = IMP_GetCmdlineArg(argc, argv, "-MPmode")) != NULL) { |
---|
109 | if (strcmp(cmode, "read") == 0) |
---|
110 | return MP_READ_MODE; |
---|
111 | else if (strcmp(cmode, "write") == 0) |
---|
112 | return MP_WRITE_MODE; |
---|
113 | else if (strcmp(cmode, "append") == 0) |
---|
114 | return MP_APPEND_MODE; |
---|
115 | } |
---|
116 | |
---|
117 | return MP_NO_SUCH_FILE_MODE; |
---|
118 | } |
---|
119 | |
---|
120 | |
---|
121 | /**** FILE transport "device" "public" routines ******/ |
---|
122 | #ifdef __STDC__ |
---|
123 | MP_Boolean_t file_get_status(MP_Link_pt link, MP_LinkStatus_t status_to_check) |
---|
124 | #else |
---|
125 | MP_Boolean_t file_get_status(link, status_to_check) |
---|
126 | MP_Link_pt link; |
---|
127 | MP_LinkStatus_t status_to_check; |
---|
128 | #endif |
---|
129 | { |
---|
130 | |
---|
131 | #ifndef __WIN32__ |
---|
132 | int filepos, fd = fileno((*(MP_FILE_t *)(link->transp.private1)).fptr); |
---|
133 | struct stat statbuf; |
---|
134 | #endif /* __WIN32__ */ |
---|
135 | |
---|
136 | #ifdef MP_DEBUG |
---|
137 | printf("file_get_status: entering for link %d\n", link->link_id); |
---|
138 | #endif |
---|
139 | |
---|
140 | #ifdef __WIN32__ |
---|
141 | switch (status_to_check) |
---|
142 | { |
---|
143 | case MP_LinkReadyReading: |
---|
144 | return MP_TRUE; |
---|
145 | |
---|
146 | case MP_LinkReadyWriting: |
---|
147 | return MP_TRUE; |
---|
148 | |
---|
149 | default: |
---|
150 | sprintf( |
---|
151 | log_msg, "file_get_status: illegal option %d", |
---|
152 | status_to_check |
---|
153 | ); |
---|
154 | MP_LogEvent(link, MP_ERROR_EVENT, log_msg); |
---|
155 | return MP_FALSE; |
---|
156 | } |
---|
157 | |
---|
158 | #else /* not __WIN32__ */ |
---|
159 | |
---|
160 | switch (status_to_check) { |
---|
161 | case MP_LinkReadyReading: |
---|
162 | if ((*(MP_FILE_t *)(link->transp.private1)).access_mode |
---|
163 | == MP_READ_MODE) { |
---|
164 | fstat(fd, &statbuf); |
---|
165 | filepos = ftell((*(MP_FILE_t *)(link->transp.private1)).fptr); |
---|
166 | return (!(filepos == statbuf.st_size)); |
---|
167 | } |
---|
168 | else { |
---|
169 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
170 | "file_get_status: can't check read status on a file opened for writing"); |
---|
171 | return MP_FALSE; |
---|
172 | } |
---|
173 | |
---|
174 | case MP_LinkReadyWriting: |
---|
175 | if ((*(MP_FILE_t *)(link->transp.private1)).access_mode |
---|
176 | == MP_WRITE_MODE) |
---|
177 | return MP_TRUE; |
---|
178 | else { |
---|
179 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
180 | "file_get_status: can't check write status on a file opened for reading"); |
---|
181 | return MP_FALSE; |
---|
182 | } |
---|
183 | |
---|
184 | default: |
---|
185 | sprintf(log_msg,"file_get_status: illegal option %d", |
---|
186 | status_to_check); |
---|
187 | MP_LogEvent(link, MP_ERROR_EVENT, log_msg); |
---|
188 | return MP_FALSE; |
---|
189 | } |
---|
190 | #endif /* not __WIN32__ */ |
---|
191 | |
---|
192 | } |
---|
193 | |
---|
194 | |
---|
195 | |
---|
196 | #ifdef __STDC__ |
---|
197 | long file_read(MP_Link_pt link, |
---|
198 | char * buf, |
---|
199 | long len) |
---|
200 | #else |
---|
201 | long file_read(link, buf, len) |
---|
202 | MP_Link_pt link; |
---|
203 | char * buf; |
---|
204 | long len; |
---|
205 | #endif |
---|
206 | { |
---|
207 | int fd = fileno((*(MP_FILE_t *)(link->transp.private1)).fptr); |
---|
208 | FILE *fptr = (*(MP_FILE_t *)(link->transp.private1)).fptr; |
---|
209 | #ifndef __WIN32__ |
---|
210 | fd_set mask, readfds; |
---|
211 | /* fd_set mask = 1 << fd, readfds; */ |
---|
212 | #endif |
---|
213 | |
---|
214 | #ifdef MP_DEBUG |
---|
215 | printf("file_read: entering -len = %d\n", len); |
---|
216 | fflush(stdout); |
---|
217 | #endif |
---|
218 | |
---|
219 | #ifndef __WIN32__ |
---|
220 | FD_ZERO(&mask); |
---|
221 | FD_SET(fd, &mask); |
---|
222 | while (MP_TRUE) { |
---|
223 | readfds = mask; |
---|
224 | switch (SELECT(FD_SETSIZE, &readfds, NULL, NULL, NULL)) { |
---|
225 | case 0: |
---|
226 | MP_LogEvent(link, MP_ERROR_EVENT, "file_read: timed out"); |
---|
227 | return -1; |
---|
228 | |
---|
229 | case -1: |
---|
230 | if (errno == EINTR) |
---|
231 | continue; |
---|
232 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
233 | "file_read: something bad happened with select()"); |
---|
234 | return -1; |
---|
235 | } |
---|
236 | if (FD_ISSET(fd, &readfds)) |
---|
237 | break; |
---|
238 | } |
---|
239 | #endif /* __WIN32__ */ |
---|
240 | |
---|
241 | if ((len = fread(buf, 1, len, fptr)) == -1) { |
---|
242 | MP_LogEvent(link, MP_ERROR_EVENT, "file_read: problem with the read"); |
---|
243 | return MP_SetError(link, MP_CantReadLink); |
---|
244 | } |
---|
245 | |
---|
246 | #ifndef NO_LOGGING |
---|
247 | if (link->logmask & MP_LOG_READ_EVENTS) { |
---|
248 | sprintf(log_msg, "file_read: read %ld bytes", len); |
---|
249 | MP_LogEvent(link, MP_READ_EVENT, log_msg); |
---|
250 | } |
---|
251 | #endif |
---|
252 | |
---|
253 | #ifdef MP_DEBUG |
---|
254 | printf("file_read: exiting - read %d bytes \n", len); |
---|
255 | fflush(stdout); |
---|
256 | #endif |
---|
257 | |
---|
258 | return len; |
---|
259 | } |
---|
260 | |
---|
261 | |
---|
262 | |
---|
263 | |
---|
264 | #ifdef __STDC__ |
---|
265 | long file_write(MP_Link_pt link, char * buf, long len) |
---|
266 | #else |
---|
267 | long file_write(link, buf, len) |
---|
268 | MP_Link_pt link; |
---|
269 | char * buf; |
---|
270 | long len; |
---|
271 | #endif |
---|
272 | { |
---|
273 | long cnt, fd = fileno((*(MP_FILE_t *)(link->transp.private1)).fptr); |
---|
274 | FILE * fptr = (*(MP_FILE_t *)(link->transp.private1)).fptr; |
---|
275 | |
---|
276 | #ifndef __WIN32__ |
---|
277 | fd_set mask, writefs; |
---|
278 | #endif |
---|
279 | |
---|
280 | #ifdef MP_DEBUG |
---|
281 | fprintf(stderr, "file_write: entering -len = %d\n", len); |
---|
282 | fflush(stderr); |
---|
283 | #endif |
---|
284 | |
---|
285 | #ifdef __WIN32__ |
---|
286 | |
---|
287 | /* |
---|
288 | * There's not much we can do to check if the file is ready for writing. |
---|
289 | * If it isn't we'll know it later. |
---|
290 | */ |
---|
291 | |
---|
292 | #else /* not __WIN32__ */ |
---|
293 | |
---|
294 | FD_ZERO(&mask); |
---|
295 | FD_SET(fd, &mask); |
---|
296 | |
---|
297 | while (MP_TRUE) { |
---|
298 | writefs = mask; |
---|
299 | switch (SELECT(FD_SETSIZE, NULL, &writefs, NULL, NULL)) { |
---|
300 | case 0: |
---|
301 | MP_LogEvent(link, MP_ERROR_EVENT, "file_write: timed out"); |
---|
302 | return -1; |
---|
303 | |
---|
304 | case -1: |
---|
305 | if (errno == EINTR) |
---|
306 | continue; |
---|
307 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
308 | "file_write: something bad happened with select()"); |
---|
309 | return -1; |
---|
310 | } |
---|
311 | if (FD_ISSET(fd, &writefs)) |
---|
312 | break; |
---|
313 | } |
---|
314 | #endif /* __WIN32__ */ |
---|
315 | |
---|
316 | if ((cnt = fwrite(buf, 1, len, fptr)) != len) { |
---|
317 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
318 | "file_write: can't do write:"); |
---|
319 | return MP_SetError(link, MP_CantWriteLink); |
---|
320 | } |
---|
321 | |
---|
322 | #ifndef NO_LOGGING |
---|
323 | if (link->logmask & MP_LOG_WRITE_EVENTS) { |
---|
324 | sprintf(log_msg, "file_write: wrote %ld bytes", cnt); |
---|
325 | MP_LogEvent(link, MP_WRITE_EVENT, log_msg); |
---|
326 | } |
---|
327 | #endif |
---|
328 | |
---|
329 | #ifdef MP_DEBUG |
---|
330 | fprintf(stderr, "file_write: exiting - wrote %d bytes \n", cnt); |
---|
331 | fflush(stderr); |
---|
332 | #endif |
---|
333 | |
---|
334 | return len; |
---|
335 | } |
---|
336 | |
---|
337 | MP_Status_t |
---|
338 | #ifdef __STDC__ |
---|
339 | file_flush(MP_Link_pt link) |
---|
340 | #else |
---|
341 | file_flush(link) |
---|
342 | MP_Link_pt link; |
---|
343 | #endif |
---|
344 | { |
---|
345 | #ifdef MP_DEBUG |
---|
346 | fprintf(stderr, "file_flush: entering\n"); |
---|
347 | fflush(stderr); |
---|
348 | #endif |
---|
349 | |
---|
350 | if (fflush((*(MP_FILE_t *)(link->transp.private1)).fptr) != 0) |
---|
351 | return MP_Failure; |
---|
352 | |
---|
353 | #ifdef MP_DEBUG |
---|
354 | fprintf(stderr, "file_flush: exiting \n"); |
---|
355 | fflush(stderr); |
---|
356 | #endif |
---|
357 | return MP_ClearError(link); |
---|
358 | } |
---|
359 | |
---|
360 | |
---|
361 | /*********************************************************************** |
---|
362 | * FUNCTION: file_open_connection |
---|
363 | * INPUT: link - pointer to link structure for this connection |
---|
364 | * argc - number of arguments in argv |
---|
365 | * argv - arguments as strings |
---|
366 | * OUTPUT: Success: MP_Success |
---|
367 | * link established as a file connection |
---|
368 | * Failure: MP_Failure |
---|
369 | * OPERATION: Determine the file mode we are using and attempt to |
---|
370 | * establish a connection accordingly. We could fail |
---|
371 | * for any of a number of reasons. Bad hostname, bad |
---|
372 | * port number, missing arguments.... |
---|
373 | ***********************************************************************/ |
---|
374 | #ifdef __STDC__ |
---|
375 | MP_Status_t file_open_connection(MP_Link_pt link, |
---|
376 | int argc, |
---|
377 | char **argv) |
---|
378 | #else |
---|
379 | MP_Status_t file_open_connection(link, argc, argv) |
---|
380 | MP_Link_pt link; |
---|
381 | int argc; |
---|
382 | char **argv; |
---|
383 | #endif |
---|
384 | { |
---|
385 | char *cmode, *fname, *mode_open_flag = NULL; |
---|
386 | MP_FILE_t *file; |
---|
387 | |
---|
388 | #ifdef MP_DEBUG |
---|
389 | fprintf(stderr, "file_open_connection: entering\n"); |
---|
390 | fflush(stderr); |
---|
391 | #endif |
---|
392 | |
---|
393 | TEST_LINK_NULL(link); |
---|
394 | if (file_init_transport(link) != MP_Success) |
---|
395 | return MP_Failure; |
---|
396 | |
---|
397 | file = (MP_FILE_t *)(link->transp.private1); |
---|
398 | if ((fname = IMP_GetCmdlineArg(argc, argv, "-MPfile")) == NULL) { |
---|
399 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
400 | "file_open_connection: error opening file:" |
---|
401 | " bad or missing -MPfile option"); |
---|
402 | return MP_SetError(link, MP_Failure); |
---|
403 | } |
---|
404 | file->access_mode = get_file_mode(argc, argv); |
---|
405 | switch (file->access_mode) { |
---|
406 | case MP_NO_SUCH_FILE_MODE: |
---|
407 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
408 | "file_open_connection: error opening file:" |
---|
409 | " bad or missing -MPmode option"); |
---|
410 | return MP_SetError(link, MP_Failure); |
---|
411 | |
---|
412 | case MP_WRITE_MODE: |
---|
413 | mode_open_flag = "wb"; |
---|
414 | break; |
---|
415 | |
---|
416 | case MP_READ_MODE: |
---|
417 | mode_open_flag = "rb"; |
---|
418 | break; |
---|
419 | |
---|
420 | case MP_APPEND_MODE: |
---|
421 | mode_open_flag = "ab"; |
---|
422 | break; |
---|
423 | } |
---|
424 | |
---|
425 | if ((file->fptr = fopen(fname, mode_open_flag)) == NULL) { |
---|
426 | char *errmsg = NULL; |
---|
427 | cmode = IMP_GetCmdlineArg(argc, argv, "-MPmode"); |
---|
428 | errmsg = (char *) IMP_RawMemAllocFnc(strlen(fname) + 80); |
---|
429 | sprintf(errmsg, "file_open_connection: Cannot open file %s " |
---|
430 | "in %s mode", fname, cmode); |
---|
431 | MP_LogEvent(link, MP_ERROR_EVENT, errmsg); |
---|
432 | IMP_RawMemFreeFnc(errmsg); |
---|
433 | return MP_SetError(link, MP_CantOpenFile); |
---|
434 | } |
---|
435 | |
---|
436 | file->fname = IMP_RawMemAllocFnc(strlen(fname) + 1); |
---|
437 | if (file->fname == NULL) { |
---|
438 | MP_LogEvent(link, MP_ERROR_EVENT, |
---|
439 | "file_open_connection: memory allocation error"); |
---|
440 | return MP_SetError(link, MP_Failure); |
---|
441 | } |
---|
442 | strcpy(file->fname, fname); |
---|
443 | |
---|
444 | #ifndef NO_LOGGING |
---|
445 | if (link->logmask & MP_LOG_INIT_EVENTS) { |
---|
446 | char *msg = NULL; |
---|
447 | cmode = IMP_GetCmdlineArg(argc, argv, "-MPmode"); |
---|
448 | msg = IMP_RawMemAllocFnc(strlen(file->fname) + strlen(cmode) + 48); |
---|
449 | sprintf(msg, "file_open_connection: opened file %s in %s mode", |
---|
450 | file->fname, cmode); |
---|
451 | MP_LogEvent(link, MP_INIT_EVENT, msg); |
---|
452 | IMP_RawMemFreeFnc(msg); |
---|
453 | } |
---|
454 | #endif |
---|
455 | |
---|
456 | #ifdef MP_DEBUG |
---|
457 | fprintf(stderr, "file_open_connection: exiting, filename is %s\n", |
---|
458 | file->fname); |
---|
459 | fflush(stderr); |
---|
460 | #endif |
---|
461 | |
---|
462 | RETURN_OK(link); |
---|
463 | } |
---|
464 | |
---|
465 | |
---|
466 | |
---|
467 | /*********************************************************************** |
---|
468 | * FUNCTION: file_close_connection |
---|
469 | * INPUT: link - pointer to the link structure for this connection |
---|
470 | * OUTPUT: Success: MP_Success |
---|
471 | * Release the file structure pointed to by private1. |
---|
472 | * Failure: MP_Failure |
---|
473 | * OPERATION: |
---|
474 | ***********************************************************************/ |
---|
475 | #ifdef __STDC__ |
---|
476 | MP_Status_t file_close_connection(MP_Link_pt link) |
---|
477 | #else |
---|
478 | MP_Status_t file_close_connection(link) |
---|
479 | MP_Link_pt link; |
---|
480 | #endif |
---|
481 | { |
---|
482 | MP_FILE_t *file; |
---|
483 | |
---|
484 | #ifdef MP_DEBUG |
---|
485 | fprintf(stderr, "file_close_connection: entering\n"); |
---|
486 | fflush(stderr); |
---|
487 | #endif |
---|
488 | |
---|
489 | TEST_LINK_NULL(link); |
---|
490 | file = (MP_FILE_t *)(link->transp.private1); |
---|
491 | if (file == NULL) |
---|
492 | return MP_SetError(link, MP_NullTransport); |
---|
493 | |
---|
494 | fclose(file->fptr); |
---|
495 | IMP_RawMemFreeFnc(file->fname); |
---|
496 | |
---|
497 | IMP_MemFreeFnc(file, sizeof(MP_FILE_t)); |
---|
498 | link->transp.private1 = NULL; |
---|
499 | |
---|
500 | #ifdef MP_DEBUG |
---|
501 | fprintf(stderr, "file_close_connection: exiting\n"); |
---|
502 | fflush(stderr); |
---|
503 | #endif |
---|
504 | |
---|
505 | RETURN_OK(link); |
---|
506 | } |
---|
507 | |
---|
508 | |
---|
509 | |
---|
510 | |
---|
511 | /*********************************************************************** |
---|
512 | * FUNCTION: file_init_transport |
---|
513 | * INPUT: link - pointer to the link structure for this connection |
---|
514 | * OUTPUT: Success: MP_Success and link structure initialized for the |
---|
515 | * file input or output. |
---|
516 | * Failure: MP_Failure |
---|
517 | * OPERATION: If there are no problems, allocate a file structure and |
---|
518 | * attach it to the private pointer inside link. |
---|
519 | ***********************************************************************/ |
---|
520 | #ifdef __STDC__ |
---|
521 | MP_Status_t file_init_transport(MP_Link_pt link) |
---|
522 | #else |
---|
523 | MP_Status_t file_init_transport(link) |
---|
524 | MP_Link_pt link; |
---|
525 | #endif |
---|
526 | { |
---|
527 | #ifdef MP_DEBUG |
---|
528 | fprintf(stderr, "file_init_transport: entering\n"); |
---|
529 | fflush(stderr); |
---|
530 | #endif |
---|
531 | |
---|
532 | TEST_LINK_NULL(link); |
---|
533 | link->transp.transp_dev = MP_FileTransportDev; |
---|
534 | link->transp.transp_ops = &file_ops; |
---|
535 | link->transp.private1 = (char *)IMP_MemAllocFnc(sizeof(MP_FILE_t)); |
---|
536 | TEST_MEM_ALLOC_ERROR(link, link->transp.private1); |
---|
537 | /* link->link_bigint_format = MP_GMP; |
---|
538 | * link->link_bigreal_format = MP_GMP; |
---|
539 | */ |
---|
540 | |
---|
541 | #ifdef MP_DEBUG |
---|
542 | fprintf(stderr, "file_init_transport: exiting\n"); |
---|
543 | fflush(stderr); |
---|
544 | #endif |
---|
545 | |
---|
546 | RETURN_OK(link); |
---|
547 | } |
---|
548 | |
---|
549 | |
---|