1 /*************************************************************************** 2 * _ _ ____ _ 3 * Project ___| | | | _ \| | 4 * / __| | | | |_) | | 5 * | (__| |_| | _ <| |___ 6 * \___|\___/|_| \_\_____| 7 * 8 * Copyright (C) 1998 - 2016, Daniel Stenberg, <daniel (at) haxx.se>, et al. 9 * 10 * This software is licensed as described in the file COPYING, which 11 * you should have received as part of this distribution. The terms 12 * are also available at https://curl.haxx.se/docs/copyright.html. 13 * 14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell 15 * copies of the Software, and permit persons to whom the Software is 16 * furnished to do so, under the terms of the COPYING file. 17 * 18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 19 * KIND, either express or implied. 20 * 21 ***************************************************************************/ 22 #include "tool_setup.h" 23 24 #if defined(MSDOS) || defined(WIN32) 25 26 #if defined(HAVE_LIBGEN_H) && defined(HAVE_BASENAME) 27 # include <libgen.h> 28 #endif 29 30 #ifdef WIN32 31 # include "tool_cfgable.h" 32 # include "tool_libinfo.h" 33 #endif 34 35 #include "tool_bname.h" 36 #include "tool_doswin.h" 37 38 #include "memdebug.h" /* keep this as LAST include */ 39 40 /* 41 * Macros ALWAYS_TRUE and ALWAYS_FALSE are used to avoid compiler warnings. 42 */ 43 44 #define ALWAYS_TRUE (1) 45 #define ALWAYS_FALSE (0) 46 47 #if defined(_MSC_VER) && !defined(__POCC__) 48 # undef ALWAYS_TRUE 49 # undef ALWAYS_FALSE 50 # if (_MSC_VER < 1500) 51 # define ALWAYS_TRUE (0, 1) 52 # define ALWAYS_FALSE (1, 0) 53 # else 54 # define ALWAYS_TRUE \ 55 __pragma(warning(push)) \ 56 __pragma(warning(disable:4127)) \ 57 (1) \ 58 __pragma(warning(pop)) 59 # define ALWAYS_FALSE \ 60 __pragma(warning(push)) \ 61 __pragma(warning(disable:4127)) \ 62 (0) \ 63 __pragma(warning(pop)) 64 # endif 65 #endif 66 67 #ifdef WIN32 68 # undef PATH_MAX 69 # define PATH_MAX MAX_PATH 70 #endif 71 72 #ifndef S_ISCHR 73 # ifdef S_IFCHR 74 # define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) 75 # else 76 # define S_ISCHR(m) (0) /* cannot tell if file is a device */ 77 # endif 78 #endif 79 80 #ifdef WIN32 81 # define _use_lfn(f) ALWAYS_TRUE /* long file names always available */ 82 #elif !defined(__DJGPP__) || (__DJGPP__ < 2) /* DJGPP 2.0 has _use_lfn() */ 83 # define _use_lfn(f) ALWAYS_FALSE /* long file names never available */ 84 #elif defined(__DJGPP__) 85 # include <fcntl.h> /* _use_lfn(f) prototype */ 86 #endif 87 88 #ifndef UNITTESTS 89 static SANITIZEcode truncate_dryrun(const char *path, 90 const size_t truncate_pos); 91 #ifdef MSDOS 92 static SANITIZEcode msdosify(char **const sanitized, const char *file_name, 93 int flags); 94 #endif 95 static SANITIZEcode rename_if_reserved_dos_device_name(char **const sanitized, 96 const char *file_name, 97 int flags); 98 #endif /* !UNITTESTS (static declarations used if no unit tests) */ 99 100 101 /* 102 Sanitize a file or path name. 103 104 All banned characters are replaced by underscores, for example: 105 f?*foo => f__foo 106 f:foo::$DATA => f_foo__$DATA 107 f:\foo:bar => f__foo_bar 108 f:\foo:bar => f:\foo:bar (flag SANITIZE_ALLOW_PATH) 109 110 This function was implemented according to the guidelines in 'Naming Files, 111 Paths, and Namespaces' section 'Naming Conventions'. 112 https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx 113 114 Flags 115 ----- 116 SANITIZE_ALLOW_COLONS: Allow colons. 117 Without this flag colons are sanitized. 118 119 SANITIZE_ALLOW_PATH: Allow path separators and colons. 120 Without this flag path separators and colons are sanitized. 121 122 SANITIZE_ALLOW_RESERVED: Allow reserved device names. 123 Without this flag a reserved device name is renamed (COM1 => _COM1) unless it's 124 in a UNC prefixed path. 125 126 SANITIZE_ALLOW_TRUNCATE: Allow truncating a long filename. 127 Without this flag if the sanitized filename or path will be too long an error 128 occurs. With this flag the filename --and not any other parts of the path-- may 129 be truncated to at least a single character. A filename followed by an 130 alternate data stream (ADS) cannot be truncated in any case. 131 132 Success: (SANITIZE_ERR_OK) *sanitized points to a sanitized copy of file_name. 133 Failure: (!= SANITIZE_ERR_OK) *sanitized is NULL. 134 */ 135 SANITIZEcode sanitize_file_name(char **const sanitized, const char *file_name, 136 int flags) 137 { 138 char *p, *target; 139 size_t len; 140 SANITIZEcode sc; 141 size_t max_sanitized_len; 142 143 if(!sanitized) 144 return SANITIZE_ERR_BAD_ARGUMENT; 145 146 *sanitized = NULL; 147 148 if(!file_name) 149 return SANITIZE_ERR_BAD_ARGUMENT; 150 151 if((flags & SANITIZE_ALLOW_PATH)) { 152 #ifndef MSDOS 153 if(file_name[0] == '\\' && file_name[1] == '\\') 154 /* UNC prefixed path \\ (eg \\?\C:\foo) */ 155 max_sanitized_len = 32767-1; 156 else 157 #endif 158 max_sanitized_len = PATH_MAX-1; 159 } 160 else 161 /* The maximum length of a filename. 162 FILENAME_MAX is often the same as PATH_MAX, in other words it is 260 and 163 does not discount the path information therefore we shouldn't use it. */ 164 max_sanitized_len = (PATH_MAX-1 > 255) ? 255 : PATH_MAX-1; 165 166 len = strlen(file_name); 167 if(len > max_sanitized_len) { 168 if(!(flags & SANITIZE_ALLOW_TRUNCATE) || 169 truncate_dryrun(file_name, max_sanitized_len)) 170 return SANITIZE_ERR_INVALID_PATH; 171 172 len = max_sanitized_len; 173 } 174 175 target = malloc(len + 1); 176 if(!target) 177 return SANITIZE_ERR_OUT_OF_MEMORY; 178 179 strncpy(target, file_name, len); 180 target[len] = '\0'; 181 182 #ifndef MSDOS 183 if((flags & SANITIZE_ALLOW_PATH) && !strncmp(target, "\\\\?\\", 4)) 184 /* Skip the literal path prefix \\?\ */ 185 p = target + 4; 186 else 187 #endif 188 p = target; 189 190 /* replace control characters and other banned characters */ 191 for(; *p; ++p) { 192 const char *banned; 193 194 if((1 <= *p && *p <= 31) || 195 (!(flags & (SANITIZE_ALLOW_COLONS|SANITIZE_ALLOW_PATH)) && *p == ':') || 196 (!(flags & SANITIZE_ALLOW_PATH) && (*p == '/' || *p == '\\'))) { 197 *p = '_'; 198 continue; 199 } 200 201 for(banned = "|<>\"?*"; *banned; ++banned) { 202 if(*p == *banned) { 203 *p = '_'; 204 break; 205 } 206 } 207 } 208 209 /* remove trailing spaces and periods if not allowing paths */ 210 if(!(flags & SANITIZE_ALLOW_PATH) && len) { 211 char *clip = NULL; 212 213 p = &target[len]; 214 do { 215 --p; 216 if(*p != ' ' && *p != '.') 217 break; 218 clip = p; 219 } while(p != target); 220 221 if(clip) { 222 *clip = '\0'; 223 len = clip - target; 224 } 225 } 226 227 #ifdef MSDOS 228 sc = msdosify(&p, target, flags); 229 free(target); 230 if(sc) 231 return sc; 232 target = p; 233 len = strlen(target); 234 235 if(len > max_sanitized_len) { 236 free(target); 237 return SANITIZE_ERR_INVALID_PATH; 238 } 239 #endif 240 241 if(!(flags & SANITIZE_ALLOW_RESERVED)) { 242 sc = rename_if_reserved_dos_device_name(&p, target, flags); 243 free(target); 244 if(sc) 245 return sc; 246 target = p; 247 len = strlen(target); 248 249 if(len > max_sanitized_len) { 250 free(target); 251 return SANITIZE_ERR_INVALID_PATH; 252 } 253 } 254 255 *sanitized = target; 256 return SANITIZE_ERR_OK; 257 } 258 259 260 /* 261 Test if truncating a path to a file will leave at least a single character in 262 the filename. Filenames suffixed by an alternate data stream can't be 263 truncated. This performs a dry run, nothing is modified. 264 265 Good truncate_pos 9: C:\foo\bar => C:\foo\ba 266 Good truncate_pos 6: C:\foo => C:\foo 267 Good truncate_pos 5: C:\foo => C:\fo 268 Bad* truncate_pos 5: C:foo => C:foo 269 Bad truncate_pos 5: C:\foo:ads => C:\fo 270 Bad truncate_pos 9: C:\foo:ads => C:\foo:ad 271 Bad truncate_pos 5: C:\foo\bar => C:\fo 272 Bad truncate_pos 5: C:\foo\ => C:\fo 273 Bad truncate_pos 7: C:\foo\ => C:\foo\ 274 Error truncate_pos 7: C:\foo => (pos out of range) 275 Bad truncate_pos 1: C:\foo\ => C 276 277 * C:foo is ambiguous, C could end up being a drive or file therefore something 278 like C:superlongfilename can't be truncated. 279 280 Returns 281 SANITIZE_ERR_OK: Good -- 'path' can be truncated 282 SANITIZE_ERR_INVALID_PATH: Bad -- 'path' cannot be truncated 283 != SANITIZE_ERR_OK && != SANITIZE_ERR_INVALID_PATH: Error 284 */ 285 SANITIZEcode truncate_dryrun(const char *path, const size_t truncate_pos) 286 { 287 size_t len; 288 289 if(!path) 290 return SANITIZE_ERR_BAD_ARGUMENT; 291 292 len = strlen(path); 293 294 if(truncate_pos > len) 295 return SANITIZE_ERR_BAD_ARGUMENT; 296 297 if(!len || !truncate_pos) 298 return SANITIZE_ERR_INVALID_PATH; 299 300 if(strpbrk(&path[truncate_pos - 1], "\\/:")) 301 return SANITIZE_ERR_INVALID_PATH; 302 303 /* C:\foo can be truncated but C:\foo:ads can't */ 304 if(truncate_pos > 1) { 305 const char *p = &path[truncate_pos - 1]; 306 do { 307 --p; 308 if(*p == ':') 309 return SANITIZE_ERR_INVALID_PATH; 310 } while(p != path && *p != '\\' && *p != '/'); 311 } 312 313 return SANITIZE_ERR_OK; 314 } 315 316 /* The functions msdosify, rename_if_dos_device_name and __crt0_glob_function 317 * were taken with modification from the DJGPP port of tar 1.12. They use 318 * algorithms originally from DJTAR. 319 */ 320 321 /* 322 Extra sanitization MSDOS for file_name. 323 324 This is a supporting function for sanitize_file_name. 325 326 Warning: This is an MSDOS legacy function and was purposely written in a way 327 that some path information may pass through. For example drive letter names 328 (C:, D:, etc) are allowed to pass through. For sanitizing a filename use 329 sanitize_file_name. 330 331 Success: (SANITIZE_ERR_OK) *sanitized points to a sanitized copy of file_name. 332 Failure: (!= SANITIZE_ERR_OK) *sanitized is NULL. 333 */ 334 #if defined(MSDOS) || defined(UNITTESTS) 335 SANITIZEcode msdosify(char **const sanitized, const char *file_name, 336 int flags) 337 { 338 char dos_name[PATH_MAX]; 339 static const char illegal_chars_dos[] = ".+, ;=[]" /* illegal in DOS */ 340 "|<>/\\\":?*"; /* illegal in DOS & W95 */ 341 static const char *illegal_chars_w95 = &illegal_chars_dos[8]; 342 int idx, dot_idx; 343 const char *s = file_name; 344 char *d = dos_name; 345 const char *const dlimit = dos_name + sizeof(dos_name) - 1; 346 const char *illegal_aliens = illegal_chars_dos; 347 size_t len = sizeof(illegal_chars_dos) - 1; 348 349 if(!sanitized) 350 return SANITIZE_ERR_BAD_ARGUMENT; 351 352 *sanitized = NULL; 353 354 if(!file_name) 355 return SANITIZE_ERR_BAD_ARGUMENT; 356 357 if(strlen(file_name) > PATH_MAX-1 && 358 (!(flags & SANITIZE_ALLOW_TRUNCATE) || 359 truncate_dryrun(file_name, PATH_MAX-1))) 360 return SANITIZE_ERR_INVALID_PATH; 361 362 /* Support for Windows 9X VFAT systems, when available. */ 363 if(_use_lfn(file_name)) { 364 illegal_aliens = illegal_chars_w95; 365 len -= (illegal_chars_w95 - illegal_chars_dos); 366 } 367 368 /* Get past the drive letter, if any. */ 369 if(s[0] >= 'A' && s[0] <= 'z' && s[1] == ':') { 370 *d++ = *s++; 371 *d = ((flags & (SANITIZE_ALLOW_COLONS|SANITIZE_ALLOW_PATH))) ? ':' : '_'; 372 ++d, ++s; 373 } 374 375 for(idx = 0, dot_idx = -1; *s && d < dlimit; s++, d++) { 376 if(memchr(illegal_aliens, *s, len)) { 377 378 if((flags & (SANITIZE_ALLOW_COLONS|SANITIZE_ALLOW_PATH)) && *s == ':') 379 *d = ':'; 380 else if((flags & SANITIZE_ALLOW_PATH) && (*s == '/' || *s == '\\')) 381 *d = *s; 382 /* Dots are special: DOS doesn't allow them as the leading character, 383 and a file name cannot have more than a single dot. We leave the 384 first non-leading dot alone, unless it comes too close to the 385 beginning of the name: we want sh.lex.c to become sh_lex.c, not 386 sh.lex-c. */ 387 else if(*s == '.') { 388 if((flags & SANITIZE_ALLOW_PATH) && idx == 0 && 389 (s[1] == '/' || s[1] == '\\' || 390 (s[1] == '.' && (s[2] == '/' || s[2] == '\\')))) { 391 /* Copy "./" and "../" verbatim. */ 392 *d++ = *s++; 393 if(d == dlimit) 394 break; 395 if(*s == '.') { 396 *d++ = *s++; 397 if(d == dlimit) 398 break; 399 } 400 *d = *s; 401 } 402 else if(idx == 0) 403 *d = '_'; 404 else if(dot_idx >= 0) { 405 if(dot_idx < 5) { /* 5 is a heuristic ad-hoc'ery */ 406 d[dot_idx - idx] = '_'; /* replace previous dot */ 407 *d = '.'; 408 } 409 else 410 *d = '-'; 411 } 412 else 413 *d = '.'; 414 415 if(*s == '.') 416 dot_idx = idx; 417 } 418 else if(*s == '+' && s[1] == '+') { 419 if(idx - 2 == dot_idx) { /* .c++, .h++ etc. */ 420 *d++ = 'x'; 421 if(d == dlimit) 422 break; 423 *d = 'x'; 424 } 425 else { 426 /* libg++ etc. */ 427 if(dlimit - d < 4) { 428 *d++ = 'x'; 429 if(d == dlimit) 430 break; 431 *d = 'x'; 432 } 433 else { 434 memcpy (d, "plus", 4); 435 d += 3; 436 } 437 } 438 s++; 439 idx++; 440 } 441 else 442 *d = '_'; 443 } 444 else 445 *d = *s; 446 if(*s == '/' || *s == '\\') { 447 idx = 0; 448 dot_idx = -1; 449 } 450 else 451 idx++; 452 } 453 *d = '\0'; 454 455 if(*s) { 456 /* dos_name is truncated, check that truncation requirements are met, 457 specifically truncating a filename suffixed by an alternate data stream 458 or truncating the entire filename is not allowed. */ 459 if(!(flags & SANITIZE_ALLOW_TRUNCATE) || strpbrk(s, "\\/:") || 460 truncate_dryrun(dos_name, d - dos_name)) 461 return SANITIZE_ERR_INVALID_PATH; 462 } 463 464 *sanitized = strdup(dos_name); 465 return (*sanitized ? SANITIZE_ERR_OK : SANITIZE_ERR_OUT_OF_MEMORY); 466 } 467 #endif /* MSDOS || UNITTESTS */ 468 469 /* 470 Rename file_name if it's a reserved dos device name. 471 472 This is a supporting function for sanitize_file_name. 473 474 Warning: This is an MSDOS legacy function and was purposely written in a way 475 that some path information may pass through. For example drive letter names 476 (C:, D:, etc) are allowed to pass through. For sanitizing a filename use 477 sanitize_file_name. 478 479 Success: (SANITIZE_ERR_OK) *sanitized points to a sanitized copy of file_name. 480 Failure: (!= SANITIZE_ERR_OK) *sanitized is NULL. 481 */ 482 SANITIZEcode rename_if_reserved_dos_device_name(char **const sanitized, 483 const char *file_name, 484 int flags) 485 { 486 /* We could have a file whose name is a device on MS-DOS. Trying to 487 * retrieve such a file would fail at best and wedge us at worst. We need 488 * to rename such files. */ 489 char *p, *base; 490 char fname[PATH_MAX]; 491 #ifdef MSDOS 492 struct_stat st_buf; 493 #endif 494 495 if(!sanitized) 496 return SANITIZE_ERR_BAD_ARGUMENT; 497 498 *sanitized = NULL; 499 500 if(!file_name) 501 return SANITIZE_ERR_BAD_ARGUMENT; 502 503 /* Ignore UNC prefixed paths, they are allowed to contain a reserved name. */ 504 #ifndef MSDOS 505 if((flags & SANITIZE_ALLOW_PATH) && 506 file_name[0] == '\\' && file_name[1] == '\\') { 507 size_t len = strlen(file_name); 508 *sanitized = malloc(len + 1); 509 if(!*sanitized) 510 return SANITIZE_ERR_OUT_OF_MEMORY; 511 strncpy(*sanitized, file_name, len + 1); 512 return SANITIZE_ERR_OK; 513 } 514 #endif 515 516 if(strlen(file_name) > PATH_MAX-1 && 517 (!(flags & SANITIZE_ALLOW_TRUNCATE) || 518 truncate_dryrun(file_name, PATH_MAX-1))) 519 return SANITIZE_ERR_INVALID_PATH; 520 521 strncpy(fname, file_name, PATH_MAX-1); 522 fname[PATH_MAX-1] = '\0'; 523 base = basename(fname); 524 525 /* Rename reserved device names that are known to be accessible without \\.\ 526 Examples: CON => _CON, CON.EXT => CON_EXT, CON:ADS => CON_ADS 527 https://support.microsoft.com/en-us/kb/74496 528 https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx 529 */ 530 for(p = fname; p; p = (p == fname && fname != base ? base : NULL)) { 531 size_t p_len; 532 int x = (curl_strnequal(p, "CON", 3) || 533 curl_strnequal(p, "PRN", 3) || 534 curl_strnequal(p, "AUX", 3) || 535 curl_strnequal(p, "NUL", 3)) ? 3 : 536 (curl_strnequal(p, "CLOCK$", 6)) ? 6 : 537 (curl_strnequal(p, "COM", 3) || curl_strnequal(p, "LPT", 3)) ? 538 (('1' <= p[3] && p[3] <= '9') ? 4 : 3) : 0; 539 540 if(!x) 541 continue; 542 543 /* the devices may be accessible with an extension or ADS, for 544 example CON.AIR and 'CON . AIR' and CON:AIR access console */ 545 546 for(; p[x] == ' '; ++x) 547 ; 548 549 if(p[x] == '.') { 550 p[x] = '_'; 551 continue; 552 } 553 else if(p[x] == ':') { 554 if(!(flags & (SANITIZE_ALLOW_COLONS|SANITIZE_ALLOW_PATH))) { 555 p[x] = '_'; 556 continue; 557 } 558 ++x; 559 } 560 else if(p[x]) /* no match */ 561 continue; 562 563 /* p points to 'CON' or 'CON ' or 'CON:', etc */ 564 p_len = strlen(p); 565 566 /* Prepend a '_' */ 567 if(strlen(fname) == PATH_MAX-1) { 568 --p_len; 569 if(!(flags & SANITIZE_ALLOW_TRUNCATE) || truncate_dryrun(p, p_len)) 570 return SANITIZE_ERR_INVALID_PATH; 571 p[p_len] = '\0'; 572 } 573 memmove(p + 1, p, p_len + 1); 574 p[0] = '_'; 575 ++p_len; 576 577 /* if fname was just modified then the basename pointer must be updated */ 578 if(p == fname) 579 base = basename(fname); 580 } 581 582 /* This is the legacy portion from rename_if_dos_device_name that checks for 583 reserved device names. It only works on MSDOS. On Windows XP the stat 584 check errors with EINVAL if the device name is reserved. On Windows 585 Vista/7/8 it sets mode S_IFREG (regular file or device). According to MSDN 586 stat doc the latter behavior is correct, but that doesn't help us identify 587 whether it's a reserved device name and not a regular file name. */ 588 #ifdef MSDOS 589 if(base && ((stat(base, &st_buf)) == 0) && (S_ISCHR(st_buf.st_mode))) { 590 /* Prepend a '_' */ 591 size_t blen = strlen(base); 592 if(blen) { 593 if(strlen(fname) == PATH_MAX-1) { 594 --blen; 595 if(!(flags & SANITIZE_ALLOW_TRUNCATE) || truncate_dryrun(base, blen)) 596 return SANITIZE_ERR_INVALID_PATH; 597 base[blen] = '\0'; 598 } 599 memmove(base + 1, base, blen + 1); 600 base[0] = '_'; 601 ++blen; 602 } 603 } 604 #endif 605 606 *sanitized = strdup(fname); 607 return (*sanitized ? SANITIZE_ERR_OK : SANITIZE_ERR_OUT_OF_MEMORY); 608 } 609 610 #if defined(MSDOS) && (defined(__DJGPP__) || defined(__GO32__)) 611 612 /* 613 * Disable program default argument globbing. We do it on our own. 614 */ 615 char **__crt0_glob_function(char *arg) 616 { 617 (void)arg; 618 return (char **)0; 619 } 620 621 #endif /* MSDOS && (__DJGPP__ || __GO32__) */ 622 623 #ifdef WIN32 624 625 /* 626 * Function to find CACert bundle on a Win32 platform using SearchPath. 627 * (SearchPath is already declared via inclusions done in setup header file) 628 * (Use the ASCII version instead of the unicode one!) 629 * The order of the directories it searches is: 630 * 1. application's directory 631 * 2. current working directory 632 * 3. Windows System directory (e.g. C:\windows\system32) 633 * 4. Windows Directory (e.g. C:\windows) 634 * 5. all directories along %PATH% 635 * 636 * For WinXP and later search order actually depends on registry value: 637 * HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SafeProcessSearchMode 638 */ 639 640 CURLcode FindWin32CACert(struct OperationConfig *config, 641 const char *bundle_file) 642 { 643 CURLcode result = CURLE_OK; 644 645 /* search and set cert file only if libcurl supports SSL */ 646 if(curlinfo->features & CURL_VERSION_SSL) { 647 648 DWORD res_len; 649 DWORD buf_tchar_size = PATH_MAX + 1; 650 DWORD buf_bytes_size = sizeof(TCHAR) * buf_tchar_size; 651 char *ptr = NULL; 652 653 char *buf = malloc(buf_bytes_size); 654 if(!buf) 655 return CURLE_OUT_OF_MEMORY; 656 buf[0] = '\0'; 657 658 res_len = SearchPathA(NULL, bundle_file, NULL, buf_tchar_size, buf, &ptr); 659 if(res_len > 0) { 660 Curl_safefree(config->cacert); 661 config->cacert = strdup(buf); 662 if(!config->cacert) 663 result = CURLE_OUT_OF_MEMORY; 664 } 665 666 Curl_safefree(buf); 667 } 668 669 return result; 670 } 671 672 #endif /* WIN32 */ 673 674 #endif /* MSDOS || WIN32 */ 675