1 /* Copyright (c) 2008, 2021, Oracle and/or its affiliates.
2
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License, version 2.0,
5 as published by the Free Software Foundation.
6
7 This program is also distributed with certain software (including
8 but not limited to OpenSSL) that is licensed under separate terms,
9 as designated in a particular file or component or in included license
10 documentation. The authors of MySQL hereby grant you an additional
11 permission to link the program and your derivative works with the
12 separately licensed software that they have included with MySQL.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License, version 2.0, for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
22
23 /*
24 The purpose of this file is to provide implementation of file IO routines on
25 Windows that can be thought as drop-in replacement for corresponding C runtime
26 functionality.
27
28 Compared to Windows CRT, this one
29 - does not have the same file descriptor
30 limitation (default is 16384 and can be increased further, whereas CRT poses
31 a hard limit of 2048 file descriptors)
32 - the file operations are not serialized
33 - positional IO pread/pwrite is ported here.
34 - no text mode for files, all IO is "binary"
35
36 Naming convention:
37 All routines are prefixed with my_win_, e.g Posix open() is implemented with
38 my_win_open()
39
40 Implemented are
41 - POSIX routines(e.g open, read, lseek ...)
42 - Some ANSI C stream routines (fopen, fdopen, fileno, fclose)
43 - Windows CRT equvalients (my_get_osfhandle, open_osfhandle)
44
45 Worth to note:
46 - File descriptors used here are located in a range that is not compatible
47 with CRT on purpose. Attempt to use a file descriptor from Windows CRT library
48 range in my_win_* function will be punished with assert()
49
50 - File streams (FILE *) are actually from the C runtime. The routines provided
51 here are useful only in scernarios that use low-level IO with my_win_fileno()
52 */
53
54 #ifdef _WIN32
55
56 #include "mysys_priv.h"
57 #include "my_sys.h"
58 #include "my_thread_local.h"
59 #include <share.h>
60 #include <sys/stat.h>
61
62 /* Associates a file descriptor with an existing operating-system file handle.*/
my_open_osfhandle(HANDLE handle,int oflag)63 File my_open_osfhandle(HANDLE handle, int oflag)
64 {
65 int offset= -1;
66 uint i;
67 DBUG_ENTER("my_open_osfhandle");
68
69 mysql_mutex_lock(&THR_LOCK_open);
70 for(i= MY_FILE_MIN; i < my_file_limit;i++)
71 {
72 if(my_file_info[i].fhandle == 0)
73 {
74 struct st_my_file_info *finfo= &(my_file_info[i]);
75 finfo->type= FILE_BY_OPEN;
76 finfo->fhandle= handle;
77 finfo->oflag= oflag;
78 offset= i;
79 break;
80 }
81 }
82 mysql_mutex_unlock(&THR_LOCK_open);
83 if(offset == -1)
84 errno= EMFILE; /* to many file handles open */
85 DBUG_RETURN(offset);
86 }
87
88
invalidate_fd(File fd)89 static void invalidate_fd(File fd)
90 {
91 DBUG_ENTER("invalidate_fd");
92 assert(fd >= MY_FILE_MIN && fd < (int)my_file_limit);
93 my_file_info[fd].fhandle= 0;
94 DBUG_VOID_RETURN;
95 }
96
97
98 /* Get Windows handle for a file descriptor */
my_get_osfhandle(File fd)99 HANDLE my_get_osfhandle(File fd)
100 {
101 DBUG_ENTER("my_get_osfhandle");
102 assert(fd >= MY_FILE_MIN && fd < (int)my_file_limit);
103 DBUG_RETURN(my_file_info[fd].fhandle);
104 }
105
106
my_get_open_flags(File fd)107 static int my_get_open_flags(File fd)
108 {
109 DBUG_ENTER("my_get_open_flags");
110 assert(fd >= MY_FILE_MIN && fd < (int)my_file_limit);
111 DBUG_RETURN(my_file_info[fd].oflag);
112 }
113
114
115 /*
116 Open a file with sharing. Similar to _sopen() from libc, but allows managing
117 share delete on win32
118
119 SYNOPSIS
120 my_win_sopen()
121 path file name
122 oflag operation flags
123 shflag share flag
124 pmode permission flags
125
126 RETURN VALUE
127 File descriptor of opened file if success
128 -1 and sets errno if fails.
129 */
130
my_win_sopen(const char * path,int oflag,int shflag,int pmode)131 File my_win_sopen(const char *path, int oflag, int shflag, int pmode)
132 {
133 int fh; /* handle of opened file */
134 int mask;
135 HANDLE osfh; /* OS handle of opened file */
136 DWORD fileaccess; /* OS file access (requested) */
137 DWORD fileshare; /* OS file sharing mode */
138 DWORD filecreate; /* OS method of opening/creating */
139 DWORD fileattrib; /* OS file attribute flags */
140 SECURITY_ATTRIBUTES SecurityAttributes;
141
142 DBUG_ENTER("my_win_sopen");
143
144 if (check_if_legal_filename(path))
145 {
146 errno= EACCES;
147 DBUG_RETURN(-1);
148 }
149 SecurityAttributes.nLength= sizeof(SecurityAttributes);
150 SecurityAttributes.lpSecurityDescriptor= NULL;
151 SecurityAttributes.bInheritHandle= !(oflag & _O_NOINHERIT);
152
153 /* decode the access flags */
154 switch (oflag & (_O_RDONLY | _O_WRONLY | _O_RDWR)) {
155 case _O_RDONLY: /* read access */
156 fileaccess= GENERIC_READ;
157 break;
158 case _O_WRONLY: /* write access */
159 fileaccess= GENERIC_WRITE;
160 break;
161 case _O_RDWR: /* read and write access */
162 fileaccess= GENERIC_READ | GENERIC_WRITE;
163 break;
164 default: /* error, bad oflag */
165 errno= EINVAL;
166 DBUG_RETURN(-1);
167 }
168
169 /* decode sharing flags */
170 switch (shflag) {
171 case _SH_DENYRW: /* exclusive access except delete */
172 fileshare= FILE_SHARE_DELETE;
173 break;
174 case _SH_DENYWR: /* share read and delete access */
175 fileshare= FILE_SHARE_READ | FILE_SHARE_DELETE;
176 break;
177 case _SH_DENYRD: /* share write and delete access */
178 fileshare= FILE_SHARE_WRITE | FILE_SHARE_DELETE;
179 break;
180 case _SH_DENYNO: /* share read, write and delete access */
181 fileshare= FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
182 break;
183 case _SH_DENYRWD: /* exclusive access */
184 fileshare= 0L;
185 break;
186 case _SH_DENYWRD: /* share read access */
187 fileshare= FILE_SHARE_READ;
188 break;
189 case _SH_DENYRDD: /* share write access */
190 fileshare= FILE_SHARE_WRITE;
191 break;
192 case _SH_DENYDEL: /* share read and write access */
193 fileshare= FILE_SHARE_READ | FILE_SHARE_WRITE;
194 break;
195 default: /* error, bad shflag */
196 errno= EINVAL;
197 DBUG_RETURN(-1);
198 }
199
200 /* decode open/create method flags */
201 switch (oflag & (_O_CREAT | _O_EXCL | _O_TRUNC)) {
202 case 0:
203 case _O_EXCL: /* ignore EXCL w/o CREAT */
204 filecreate= OPEN_EXISTING;
205 break;
206
207 case _O_CREAT:
208 filecreate= OPEN_ALWAYS;
209 break;
210
211 case _O_CREAT | _O_EXCL:
212 case _O_CREAT | _O_TRUNC | _O_EXCL:
213 filecreate= CREATE_NEW;
214 break;
215
216 case _O_TRUNC:
217 case _O_TRUNC | _O_EXCL: /* ignore EXCL w/o CREAT */
218 filecreate= TRUNCATE_EXISTING;
219 break;
220
221 case _O_CREAT | _O_TRUNC:
222 filecreate= CREATE_ALWAYS;
223 break;
224
225 default:
226 /* this can't happen ... all cases are covered */
227 errno= EINVAL;
228 DBUG_RETURN(-1);
229 }
230
231 /* decode file attribute flags if _O_CREAT was specified */
232 fileattrib= FILE_ATTRIBUTE_NORMAL; /* default */
233 if (oflag & _O_CREAT)
234 {
235 _umask((mask= _umask(0)));
236
237 if (!((pmode & ~mask) & _S_IWRITE))
238 fileattrib= FILE_ATTRIBUTE_READONLY;
239 }
240
241 /* Set temporary file (delete-on-close) attribute if requested. */
242 if (oflag & _O_TEMPORARY)
243 {
244 fileattrib|= FILE_FLAG_DELETE_ON_CLOSE;
245 fileaccess|= DELETE;
246 }
247
248 /* Set temporary file (delay-flush-to-disk) attribute if requested.*/
249 if (oflag & _O_SHORT_LIVED)
250 fileattrib|= FILE_ATTRIBUTE_TEMPORARY;
251
252 /* Set sequential or random access attribute if requested. */
253 if (oflag & _O_SEQUENTIAL)
254 fileattrib|= FILE_FLAG_SEQUENTIAL_SCAN;
255 else if (oflag & _O_RANDOM)
256 fileattrib|= FILE_FLAG_RANDOM_ACCESS;
257
258 /* try to open/create the file */
259 if ((osfh= CreateFile(path, fileaccess, fileshare, &SecurityAttributes,
260 filecreate, fileattrib, NULL)) == INVALID_HANDLE_VALUE)
261 {
262 /*
263 OS call to open/create file failed! map the error, release
264 the lock, and return -1. note that it's not necessary to
265 call _free_osfhnd (it hasn't been used yet).
266 */
267 my_osmaperr(GetLastError()); /* map error */
268 DBUG_RETURN(-1); /* return error to caller */
269 }
270
271 if ((fh= my_open_osfhandle(osfh,
272 oflag & (_O_APPEND | _O_RDONLY | _O_TEXT))) == -1)
273 {
274 CloseHandle(osfh);
275 }
276
277 DBUG_RETURN(fh); /* return handle */
278 }
279
280
my_win_open(const char * path,int flags)281 File my_win_open(const char *path, int flags)
282 {
283 DBUG_ENTER("my_win_open");
284 DBUG_RETURN(my_win_sopen((char *) path, flags | _O_BINARY, _SH_DENYNO,
285 _S_IREAD | S_IWRITE));
286 }
287
288
my_win_close(File fd)289 int my_win_close(File fd)
290 {
291 DBUG_ENTER("my_win_close");
292 if(CloseHandle(my_get_osfhandle(fd)))
293 {
294 invalidate_fd(fd);
295 DBUG_RETURN(0);
296 }
297 my_osmaperr(GetLastError());
298 DBUG_RETURN(-1);
299 }
300
301
my_win_pread(File Filedes,uchar * Buffer,size_t Count,my_off_t offset)302 size_t my_win_pread(File Filedes, uchar *Buffer, size_t Count, my_off_t offset)
303 {
304 DWORD nBytesRead;
305 HANDLE hFile;
306 OVERLAPPED ov= {0};
307 LARGE_INTEGER li;
308
309 DBUG_ENTER("my_win_pread");
310
311 if(!Count)
312 DBUG_RETURN(0);
313 #ifdef _WIN64
314 if(Count > UINT_MAX)
315 Count= UINT_MAX;
316 #endif
317
318 hFile= (HANDLE)my_get_osfhandle(Filedes);
319 li.QuadPart= offset;
320 ov.Offset= li.LowPart;
321 ov.OffsetHigh= li.HighPart;
322
323 if(!ReadFile(hFile, Buffer, (DWORD)Count, &nBytesRead, &ov))
324 {
325 DWORD lastError= GetLastError();
326 /*
327 ERROR_BROKEN_PIPE is returned when no more data coming
328 through e.g. a command pipe in windows : see MSDN on ReadFile.
329 */
330 if(lastError == ERROR_HANDLE_EOF || lastError == ERROR_BROKEN_PIPE)
331 DBUG_RETURN(0); /*return 0 at EOF*/
332 my_osmaperr(lastError);
333 DBUG_RETURN((size_t)-1);
334 }
335 DBUG_RETURN(nBytesRead);
336 }
337
338
my_win_read(File Filedes,uchar * Buffer,size_t Count)339 size_t my_win_read(File Filedes, uchar *Buffer, size_t Count)
340 {
341 DWORD nBytesRead;
342 HANDLE hFile;
343
344 DBUG_ENTER("my_win_read");
345 if(!Count)
346 DBUG_RETURN(0);
347 #ifdef _WIN64
348 if(Count > UINT_MAX)
349 Count= UINT_MAX;
350 #endif
351
352 hFile= (HANDLE)my_get_osfhandle(Filedes);
353
354 if(!ReadFile(hFile, Buffer, (DWORD)Count, &nBytesRead, NULL))
355 {
356 DWORD lastError= GetLastError();
357 /*
358 ERROR_BROKEN_PIPE is returned when no more data coming
359 through e.g. a command pipe in windows : see MSDN on ReadFile.
360 */
361 if(lastError == ERROR_HANDLE_EOF || lastError == ERROR_BROKEN_PIPE)
362 DBUG_RETURN(0); /*return 0 at EOF*/
363 my_osmaperr(lastError);
364 DBUG_RETURN((size_t)-1);
365 }
366 DBUG_RETURN(nBytesRead);
367 }
368
369
my_win_pwrite(File Filedes,const uchar * Buffer,size_t Count,my_off_t offset)370 size_t my_win_pwrite(File Filedes, const uchar *Buffer, size_t Count,
371 my_off_t offset)
372 {
373 DWORD nBytesWritten;
374 HANDLE hFile;
375 OVERLAPPED ov= {0};
376 LARGE_INTEGER li;
377
378 DBUG_ENTER("my_win_pwrite");
379 DBUG_PRINT("my",("Filedes: %d, Buffer: %p, Count: %llu, offset: %llu",
380 Filedes, Buffer, (ulonglong)Count, (ulonglong)offset));
381
382 if(!Count)
383 DBUG_RETURN(0);
384
385 #ifdef _WIN64
386 if(Count > UINT_MAX)
387 Count= UINT_MAX;
388 #endif
389
390 hFile= (HANDLE)my_get_osfhandle(Filedes);
391 li.QuadPart= offset;
392 ov.Offset= li.LowPart;
393 ov.OffsetHigh= li.HighPart;
394
395 if(!WriteFile(hFile, Buffer, (DWORD)Count, &nBytesWritten, &ov))
396 {
397 my_osmaperr(GetLastError());
398 DBUG_RETURN((size_t)-1);
399 }
400 else
401 DBUG_RETURN(nBytesWritten);
402 }
403
404
my_win_lseek(File fd,my_off_t pos,int whence)405 my_off_t my_win_lseek(File fd, my_off_t pos, int whence)
406 {
407 LARGE_INTEGER offset;
408 LARGE_INTEGER newpos;
409
410 DBUG_ENTER("my_win_lseek");
411
412 /* Check compatibility of Windows and Posix seek constants */
413 compile_time_assert(FILE_BEGIN == SEEK_SET && FILE_CURRENT == SEEK_CUR
414 && FILE_END == SEEK_END);
415
416 offset.QuadPart= pos;
417 if(!SetFilePointerEx(my_get_osfhandle(fd), offset, &newpos, whence))
418 {
419 my_osmaperr(GetLastError());
420 newpos.QuadPart= -1;
421 }
422 DBUG_RETURN(newpos.QuadPart);
423 }
424
425
426 #ifndef FILE_WRITE_TO_END_OF_FILE
427 #define FILE_WRITE_TO_END_OF_FILE 0xffffffff
428 #endif
my_win_write(File fd,const uchar * Buffer,size_t Count)429 size_t my_win_write(File fd, const uchar *Buffer, size_t Count)
430 {
431 DWORD nWritten;
432 OVERLAPPED ov;
433 OVERLAPPED *pov= NULL;
434 HANDLE hFile;
435
436 DBUG_ENTER("my_win_write");
437 DBUG_PRINT("my",("Filedes: %d, Buffer: %p, Count %llu", fd, Buffer,
438 (ulonglong)Count));
439
440 if(!Count)
441 DBUG_RETURN(0);
442
443 #ifdef _WIN64
444 if(Count > UINT_MAX)
445 Count= UINT_MAX;
446 #endif
447
448 if(my_get_open_flags(fd) & _O_APPEND)
449 {
450 /*
451 Atomic append to the end of file is is done by special initialization of
452 the OVERLAPPED structure. See MSDN WriteFile documentation for more info.
453 */
454 memset(&ov, 0, sizeof(ov));
455 ov.Offset= FILE_WRITE_TO_END_OF_FILE;
456 ov.OffsetHigh= -1;
457 pov= &ov;
458 }
459
460 hFile= my_get_osfhandle(fd);
461 if(!WriteFile(hFile, Buffer, (DWORD)Count, &nWritten, pov))
462 {
463 my_osmaperr(GetLastError());
464 DBUG_RETURN((size_t)-1);
465 }
466 DBUG_RETURN(nWritten);
467 }
468
469
my_win_chsize(File fd,my_off_t newlength)470 int my_win_chsize(File fd, my_off_t newlength)
471 {
472 HANDLE hFile;
473 LARGE_INTEGER length;
474 DBUG_ENTER("my_win_chsize");
475
476 hFile= (HANDLE) my_get_osfhandle(fd);
477 length.QuadPart= newlength;
478 if (!SetFilePointerEx(hFile, length , NULL , FILE_BEGIN))
479 goto err;
480 if (!SetEndOfFile(hFile))
481 goto err;
482 DBUG_RETURN(0);
483 err:
484 my_osmaperr(GetLastError());
485 set_my_errno(errno);
486 DBUG_RETURN(-1);
487 }
488
489
490 /* Get the file descriptor for stdin,stdout or stderr */
my_get_stdfile_descriptor(FILE * stream)491 static File my_get_stdfile_descriptor(FILE *stream)
492 {
493 HANDLE hFile;
494 DWORD nStdHandle;
495 DBUG_ENTER("my_get_stdfile_descriptor");
496
497 if(stream == stdin)
498 nStdHandle= STD_INPUT_HANDLE;
499 else if(stream == stdout)
500 nStdHandle= STD_OUTPUT_HANDLE;
501 else if(stream == stderr)
502 nStdHandle= STD_ERROR_HANDLE;
503 else
504 DBUG_RETURN(-1);
505
506 hFile= GetStdHandle(nStdHandle);
507 if(hFile != INVALID_HANDLE_VALUE)
508 DBUG_RETURN(my_open_osfhandle(hFile, 0));
509 DBUG_RETURN(-1);
510 }
511
512
my_win_fileno(FILE * file)513 File my_win_fileno(FILE *file)
514 {
515 HANDLE hFile= (HANDLE)_get_osfhandle(fileno(file));
516 int retval= -1;
517 uint i;
518
519 DBUG_ENTER("my_win_fileno");
520
521 for(i= MY_FILE_MIN; i < my_file_limit; i++)
522 {
523 if(my_file_info[i].fhandle == hFile)
524 {
525 retval= i;
526 break;
527 }
528 }
529 if(retval == -1)
530 /* try std stream */
531 DBUG_RETURN(my_get_stdfile_descriptor(file));
532 DBUG_RETURN(retval);
533 }
534
535
my_win_fopen(const char * filename,const char * type)536 FILE *my_win_fopen(const char *filename, const char *type)
537 {
538 FILE *file;
539 int flags= 0;
540 DBUG_ENTER("my_win_open");
541
542 /*
543 If we are not creating, then we need to use my_access to make sure
544 the file exists since Windows doesn't handle files like "com1.sym"
545 very well
546 */
547 if (check_if_legal_filename(filename))
548 {
549 errno= EACCES;
550 DBUG_RETURN(NULL);
551 }
552
553 file= fopen(filename, type);
554 if(!file)
555 DBUG_RETURN(NULL);
556
557 if(strchr(type,'a') != NULL)
558 flags= O_APPEND;
559
560 /*
561 Register file handle in my_table_info.
562 Necessary for my_fileno()
563 */
564 if(my_open_osfhandle((HANDLE)_get_osfhandle(fileno(file)), flags) < 0)
565 {
566 fclose(file);
567 DBUG_RETURN(NULL);
568 }
569 DBUG_RETURN(file);
570 }
571
572
my_win_fdopen(File fd,const char * type)573 FILE * my_win_fdopen(File fd, const char *type)
574 {
575 FILE *file;
576 int crt_fd;
577 int flags= 0;
578
579 DBUG_ENTER("my_win_fdopen");
580
581 if(strchr(type,'a') != NULL)
582 flags= O_APPEND;
583 /* Convert OS file handle to CRT file descriptor and then call fdopen*/
584 crt_fd= _open_osfhandle((intptr_t)my_get_osfhandle(fd), flags);
585 if(crt_fd < 0)
586 file= NULL;
587 else
588 file= fdopen(crt_fd, type);
589 DBUG_RETURN(file);
590 }
591
592
my_win_fclose(FILE * file)593 int my_win_fclose(FILE *file)
594 {
595 File fd;
596
597 DBUG_ENTER("my_win_close");
598 fd= my_fileno(file);
599 if(fd < 0)
600 DBUG_RETURN(-1);
601 if(fclose(file) < 0)
602 DBUG_RETURN(-1);
603 invalidate_fd(fd);
604 DBUG_RETURN(0);
605 }
606
607
608
609 /*
610 Quick and dirty my_fstat() implementation for Windows.
611 Use CRT fstat on temporarily allocated file descriptor.
612 Patch file size, because size that fstat returns is not
613 reliable (may be outdated)
614 */
my_win_fstat(File fd,struct _stati64 * buf)615 int my_win_fstat(File fd, struct _stati64 *buf)
616 {
617 int crt_fd;
618 int retval;
619 HANDLE hFile, hDup;
620
621 DBUG_ENTER("my_win_fstat");
622
623 hFile= my_get_osfhandle(fd);
624 if(!DuplicateHandle( GetCurrentProcess(), hFile, GetCurrentProcess(),
625 &hDup ,0,FALSE,DUPLICATE_SAME_ACCESS))
626 {
627 my_osmaperr(GetLastError());
628 DBUG_RETURN(-1);
629 }
630 if ((crt_fd= _open_osfhandle((intptr_t)hDup,0)) < 0)
631 DBUG_RETURN(-1);
632
633 retval= _fstati64(crt_fd, buf);
634 if(retval == 0)
635 {
636 /* File size returned by stat is not accurate (may be outdated), fix it*/
637 GetFileSizeEx(hDup, (PLARGE_INTEGER) (&(buf->st_size)));
638 }
639 _close(crt_fd);
640 DBUG_RETURN(retval);
641 }
642
643
644
my_win_stat(const char * path,struct _stati64 * buf)645 int my_win_stat( const char *path, struct _stati64 *buf)
646 {
647 DBUG_ENTER("my_win_stat");
648 if(_stati64( path, buf) == 0)
649 {
650 /* File size returned by stat is not accurate (may be outdated), fix it*/
651 WIN32_FILE_ATTRIBUTE_DATA data;
652 if (GetFileAttributesEx(path, GetFileExInfoStandard, &data))
653 {
654 LARGE_INTEGER li;
655 li.LowPart= data.nFileSizeLow;
656 li.HighPart= data.nFileSizeHigh;
657 buf->st_size= li.QuadPart;
658 }
659 DBUG_RETURN(0);
660 }
661 DBUG_RETURN(-1);
662 }
663
664
665
my_win_fsync(File fd)666 int my_win_fsync(File fd)
667 {
668 DBUG_ENTER("my_win_fsync");
669 if(FlushFileBuffers(my_get_osfhandle(fd)))
670 DBUG_RETURN(0);
671 my_osmaperr(GetLastError());
672 DBUG_RETURN(-1);
673 }
674
675
676
my_win_dup(File fd)677 int my_win_dup(File fd)
678 {
679 HANDLE hDup;
680 DBUG_ENTER("my_win_dup");
681 if (DuplicateHandle(GetCurrentProcess(), my_get_osfhandle(fd),
682 GetCurrentProcess(), &hDup, 0, FALSE, DUPLICATE_SAME_ACCESS))
683 {
684 DBUG_RETURN(my_open_osfhandle(hDup, my_get_open_flags(fd)));
685 }
686 my_osmaperr(GetLastError());
687 DBUG_RETURN(-1);
688 }
689
690 #endif /*_WIN32*/
691