1 /* 2 * I18N/language support for CUPS. 3 * 4 * Copyright 2007-2017 by Apple Inc. 5 * Copyright 1997-2007 by Easy Software Products. 6 * 7 * These coded instructions, statements, and computer programs are the 8 * property of Apple Inc. and are protected by Federal copyright 9 * law. Distribution and use rights are outlined in the file "LICENSE.txt" 10 * which should have been included with this file. If this file is 11 * missing or damaged, see the license at "http://www.cups.org/". 12 * 13 * This file is subject to the Apple OS-Developed Software exception. 14 */ 15 16 /* 17 * Include necessary headers... 18 */ 19 20 #include "cups-private.h" 21 #ifdef HAVE_LANGINFO_H 22 # include <langinfo.h> 23 #endif /* HAVE_LANGINFO_H */ 24 #ifdef WIN32 25 # include <io.h> 26 #else 27 # include <unistd.h> 28 #endif /* WIN32 */ 29 #ifdef HAVE_COREFOUNDATION_H 30 # include <CoreFoundation/CoreFoundation.h> 31 #endif /* HAVE_COREFOUNDATION_H */ 32 33 34 /* 35 * Local globals... 36 */ 37 38 static _cups_mutex_t lang_mutex = _CUPS_MUTEX_INITIALIZER; 39 /* Mutex to control access to cache */ 40 static cups_lang_t *lang_cache = NULL; 41 /* Language string cache */ 42 static const char * const lang_encodings[] = 43 { /* Encoding strings */ 44 "us-ascii", "iso-8859-1", 45 "iso-8859-2", "iso-8859-3", 46 "iso-8859-4", "iso-8859-5", 47 "iso-8859-6", "iso-8859-7", 48 "iso-8859-8", "iso-8859-9", 49 "iso-8859-10", "utf-8", 50 "iso-8859-13", "iso-8859-14", 51 "iso-8859-15", "cp874", 52 "cp1250", "cp1251", 53 "cp1252", "cp1253", 54 "cp1254", "cp1255", 55 "cp1256", "cp1257", 56 "cp1258", "koi8-r", 57 "koi8-u", "iso-8859-11", 58 "iso-8859-16", "mac", 59 "unknown", "unknown", 60 "unknown", "unknown", 61 "unknown", "unknown", 62 "unknown", "unknown", 63 "unknown", "unknown", 64 "unknown", "unknown", 65 "unknown", "unknown", 66 "unknown", "unknown", 67 "unknown", "unknown", 68 "unknown", "unknown", 69 "unknown", "unknown", 70 "unknown", "unknown", 71 "unknown", "unknown", 72 "unknown", "unknown", 73 "unknown", "unknown", 74 "unknown", "unknown", 75 "unknown", "unknown", 76 "cp932", "cp936", 77 "cp949", "cp950", 78 "cp1361", "unknown", 79 "unknown", "unknown", 80 "unknown", "unknown", 81 "unknown", "unknown", 82 "unknown", "unknown", 83 "unknown", "unknown", 84 "unknown", "unknown", 85 "unknown", "unknown", 86 "unknown", "unknown", 87 "unknown", "unknown", 88 "unknown", "unknown", 89 "unknown", "unknown", 90 "unknown", "unknown", 91 "unknown", "unknown", 92 "unknown", "unknown", 93 "unknown", "unknown", 94 "unknown", "unknown", 95 "unknown", "unknown", 96 "unknown", "unknown", 97 "unknown", "unknown", 98 "unknown", "unknown", 99 "unknown", "unknown", 100 "unknown", "unknown", 101 "unknown", "unknown", 102 "unknown", "unknown", 103 "unknown", "unknown", 104 "unknown", "unknown", 105 "unknown", "unknown", 106 "unknown", "unknown", 107 "unknown", "unknown", 108 "euc-cn", "euc-jp", 109 "euc-kr", "euc-tw", 110 "shift_jisx0213" 111 }; 112 113 #ifdef __APPLE__ 114 typedef struct 115 { 116 const char * const language; /* Language ID */ 117 const char * const locale; /* Locale ID */ 118 } _apple_language_locale_t; 119 120 static const _apple_language_locale_t apple_language_locale[] = 121 { /* Language to locale ID LUT */ 122 { "en", "en_US" }, 123 { "nb", "no" }, 124 { "nb_NO", "no" }, 125 { "zh-Hans", "zh_CN" }, 126 { "zh-Hant", "zh_TW" }, 127 { "zh-Hant_CN", "zh_TW" } 128 }; 129 #endif /* __APPLE__ */ 130 131 132 /* 133 * Local functions... 134 */ 135 136 137 #ifdef __APPLE__ 138 static const char *appleLangDefault(void); 139 # ifdef CUPS_BUNDLEDIR 140 # ifndef CF_RETURNS_RETAINED 141 # if __has_feature(attribute_cf_returns_retained) 142 # define CF_RETURNS_RETAINED __attribute__((cf_returns_retained)) 143 # else 144 # define CF_RETURNS_RETAINED 145 # endif /* __has_feature(attribute_cf_returns_retained) */ 146 # endif /* !CF_RETURNED_RETAINED */ 147 static cups_array_t *appleMessageLoad(const char *locale) 148 CF_RETURNS_RETAINED; 149 # endif /* CUPS_BUNDLEDIR */ 150 #endif /* __APPLE__ */ 151 static cups_lang_t *cups_cache_lookup(const char *name, 152 cups_encoding_t encoding); 153 static int cups_message_compare(_cups_message_t *m1, 154 _cups_message_t *m2); 155 static void cups_message_free(_cups_message_t *m); 156 static void cups_message_load(cups_lang_t *lang); 157 static void cups_unquote(char *d, const char *s); 158 159 160 #ifdef __APPLE__ 161 /* 162 * '_cupsAppleLanguage()' - Get the Apple language identifier associated with a 163 * locale ID. 164 */ 165 166 const char * /* O - Language ID */ 167 _cupsAppleLanguage(const char *locale, /* I - Locale ID */ 168 char *language,/* I - Language ID buffer */ 169 size_t langsize) /* I - Size of language ID buffer */ 170 { 171 int i; /* Looping var */ 172 CFStringRef localeid, /* CF locale identifier */ 173 langid; /* CF language identifier */ 174 175 176 /* 177 * Copy the locale name and convert, as needed, to the Apple-specific 178 * locale identifier... 179 */ 180 181 switch (strlen(locale)) 182 { 183 default : 184 /* 185 * Invalid locale... 186 */ 187 188 strlcpy(language, "en", langsize); 189 break; 190 191 case 2 : 192 strlcpy(language, locale, langsize); 193 break; 194 195 case 5 : 196 strlcpy(language, locale, langsize); 197 198 if (language[2] == '-') 199 { 200 /* 201 * Convert ll-cc to ll_CC... 202 */ 203 204 language[2] = '_'; 205 language[3] = (char)toupper(language[3] & 255); 206 language[4] = (char)toupper(language[4] & 255); 207 } 208 break; 209 } 210 211 for (i = 0; 212 i < (int)(sizeof(apple_language_locale) / 213 sizeof(apple_language_locale[0])); 214 i ++) 215 if (!strcmp(locale, apple_language_locale[i].locale)) 216 { 217 strlcpy(language, apple_language_locale[i].language, sizeof(language)); 218 break; 219 } 220 221 /* 222 * Attempt to map the locale ID to a language ID... 223 */ 224 225 if ((localeid = CFStringCreateWithCString(kCFAllocatorDefault, language, 226 kCFStringEncodingASCII)) != NULL) 227 { 228 if ((langid = CFLocaleCreateCanonicalLanguageIdentifierFromString( 229 kCFAllocatorDefault, localeid)) != NULL) 230 { 231 CFStringGetCString(langid, language, (CFIndex)langsize, kCFStringEncodingASCII); 232 CFRelease(langid); 233 } 234 235 CFRelease(localeid); 236 } 237 238 /* 239 * Return what we got... 240 */ 241 242 return (language); 243 } 244 245 246 /* 247 * '_cupsAppleLocale()' - Get the locale associated with an Apple language ID. 248 */ 249 250 const char * /* O - Locale */ 251 _cupsAppleLocale(CFStringRef languageName, /* I - Apple language ID */ 252 char *locale, /* I - Buffer for locale */ 253 size_t localesize) /* I - Size of buffer */ 254 { 255 int i; /* Looping var */ 256 CFStringRef localeName; /* Locale as a CF string */ 257 258 259 localeName = CFLocaleCreateCanonicalLocaleIdentifierFromString(kCFAllocatorDefault, languageName); 260 261 if (localeName) 262 { 263 /* 264 * Copy the locale name and tweak as needed... 265 */ 266 267 if (!CFStringGetCString(localeName, locale, (CFIndex)localesize, kCFStringEncodingASCII)) 268 *locale = '\0'; 269 270 CFRelease(localeName); 271 272 /* 273 * Map new language identifiers to locales... 274 */ 275 276 for (i = 0; 277 i < (int)(sizeof(apple_language_locale) / 278 sizeof(apple_language_locale[0])); 279 i ++) 280 { 281 if (!strcmp(locale, apple_language_locale[i].language)) 282 { 283 strlcpy(locale, apple_language_locale[i].locale, localesize); 284 break; 285 } 286 } 287 } 288 else 289 { 290 /* 291 * Just try the Apple language name... 292 */ 293 294 if (!CFStringGetCString(languageName, locale, (CFIndex)localesize, kCFStringEncodingASCII)) 295 *locale = '\0'; 296 } 297 298 if (!*locale) 299 return (NULL); 300 301 /* 302 * Convert language subtag into region subtag... 303 */ 304 305 if (locale[2] == '-') 306 locale[2] = '_'; 307 308 if (!strchr(locale, '.')) 309 strlcat(locale, ".UTF-8", localesize); 310 311 return (locale); 312 } 313 #endif /* __APPLE__ */ 314 315 316 /* 317 * '_cupsEncodingName()' - Return the character encoding name string 318 * for the given encoding enumeration. 319 */ 320 321 const char * /* O - Character encoding */ 322 _cupsEncodingName( 323 cups_encoding_t encoding) /* I - Encoding value */ 324 { 325 if (encoding < CUPS_US_ASCII || 326 encoding >= (cups_encoding_t)(sizeof(lang_encodings) / sizeof(lang_encodings[0]))) 327 { 328 DEBUG_printf(("1_cupsEncodingName(encoding=%d) = out of range (\"%s\")", 329 encoding, lang_encodings[0])); 330 return (lang_encodings[0]); 331 } 332 else 333 { 334 DEBUG_printf(("1_cupsEncodingName(encoding=%d) = \"%s\"", 335 encoding, lang_encodings[encoding])); 336 return (lang_encodings[encoding]); 337 } 338 } 339 340 341 /* 342 * 'cupsLangDefault()' - Return the default language. 343 */ 344 345 cups_lang_t * /* O - Language data */ 346 cupsLangDefault(void) 347 { 348 return (cupsLangGet(NULL)); 349 } 350 351 352 /* 353 * 'cupsLangEncoding()' - Return the character encoding (us-ascii, etc.) 354 * for the given language. 355 */ 356 357 const char * /* O - Character encoding */ 358 cupsLangEncoding(cups_lang_t *lang) /* I - Language data */ 359 { 360 if (lang == NULL) 361 return ((char*)lang_encodings[0]); 362 else 363 return ((char*)lang_encodings[lang->encoding]); 364 } 365 366 367 /* 368 * 'cupsLangFlush()' - Flush all language data out of the cache. 369 */ 370 371 void 372 cupsLangFlush(void) 373 { 374 cups_lang_t *lang, /* Current language */ 375 *next; /* Next language */ 376 377 378 /* 379 * Free all languages in the cache... 380 */ 381 382 _cupsMutexLock(&lang_mutex); 383 384 for (lang = lang_cache; lang != NULL; lang = next) 385 { 386 /* 387 * Free all messages... 388 */ 389 390 _cupsMessageFree(lang->strings); 391 392 /* 393 * Then free the language structure itself... 394 */ 395 396 next = lang->next; 397 free(lang); 398 } 399 400 lang_cache = NULL; 401 402 _cupsMutexUnlock(&lang_mutex); 403 } 404 405 406 /* 407 * 'cupsLangFree()' - Free language data. 408 * 409 * This does not actually free anything; use @link cupsLangFlush@ for that. 410 */ 411 412 void 413 cupsLangFree(cups_lang_t *lang) /* I - Language to free */ 414 { 415 _cupsMutexLock(&lang_mutex); 416 417 if (lang != NULL && lang->used > 0) 418 lang->used --; 419 420 _cupsMutexUnlock(&lang_mutex); 421 } 422 423 424 /* 425 * 'cupsLangGet()' - Get a language. 426 */ 427 428 cups_lang_t * /* O - Language data */ 429 cupsLangGet(const char *language) /* I - Language or locale */ 430 { 431 int i; /* Looping var */ 432 #ifndef __APPLE__ 433 char locale[255]; /* Copy of locale name */ 434 #endif /* !__APPLE__ */ 435 char langname[16], /* Requested language name */ 436 country[16], /* Country code */ 437 charset[16], /* Character set */ 438 *csptr, /* Pointer to CODESET string */ 439 *ptr, /* Pointer into language/charset */ 440 real[48]; /* Real language name */ 441 cups_encoding_t encoding; /* Encoding to use */ 442 cups_lang_t *lang; /* Current language... */ 443 static const char * const locale_encodings[] = 444 { /* Locale charset names */ 445 "ASCII", "ISO88591", "ISO88592", "ISO88593", 446 "ISO88594", "ISO88595", "ISO88596", "ISO88597", 447 "ISO88598", "ISO88599", "ISO885910", "UTF8", 448 "ISO885913", "ISO885914", "ISO885915", "CP874", 449 "CP1250", "CP1251", "CP1252", "CP1253", 450 "CP1254", "CP1255", "CP1256", "CP1257", 451 "CP1258", "KOI8R", "KOI8U", "ISO885911", 452 "ISO885916", "MACROMAN", "", "", 453 454 "", "", "", "", 455 "", "", "", "", 456 "", "", "", "", 457 "", "", "", "", 458 "", "", "", "", 459 "", "", "", "", 460 "", "", "", "", 461 "", "", "", "", 462 463 "CP932", "CP936", "CP949", "CP950", 464 "CP1361", "", "", "", 465 "", "", "", "", 466 "", "", "", "", 467 "", "", "", "", 468 "", "", "", "", 469 "", "", "", "", 470 "", "", "", "", 471 472 "", "", "", "", 473 "", "", "", "", 474 "", "", "", "", 475 "", "", "", "", 476 "", "", "", "", 477 "", "", "", "", 478 "", "", "", "", 479 "", "", "", "", 480 481 "EUCCN", "EUCJP", "EUCKR", "EUCTW", 482 "SHIFT_JISX0213" 483 }; 484 485 486 DEBUG_printf(("2cupsLangGet(language=\"%s\")", language)); 487 488 #ifdef __APPLE__ 489 /* 490 * Set the character set to UTF-8... 491 */ 492 493 strlcpy(charset, "UTF8", sizeof(charset)); 494 495 /* 496 * Apple's setlocale doesn't give us the user's localization 497 * preference so we have to look it up this way... 498 */ 499 500 if (!language) 501 { 502 if (!getenv("SOFTWARE") || (language = getenv("LANG")) == NULL) 503 language = appleLangDefault(); 504 505 DEBUG_printf(("4cupsLangGet: language=\"%s\"", language)); 506 } 507 508 #else 509 /* 510 * Set the charset to "unknown"... 511 */ 512 513 charset[0] = '\0'; 514 515 /* 516 * Use setlocale() to determine the currently set locale, and then 517 * fallback to environment variables to avoid setting the locale, 518 * since setlocale() is not thread-safe! 519 */ 520 521 if (!language) 522 { 523 /* 524 * First see if the locale has been set; if it is still "C" or 525 * "POSIX", use the environment to get the default... 526 */ 527 528 # ifdef LC_MESSAGES 529 ptr = setlocale(LC_MESSAGES, NULL); 530 # else 531 ptr = setlocale(LC_ALL, NULL); 532 # endif /* LC_MESSAGES */ 533 534 DEBUG_printf(("4cupsLangGet: current locale is \"%s\"", ptr)); 535 536 if (!ptr || !strcmp(ptr, "C") || !strcmp(ptr, "POSIX")) 537 { 538 /* 539 * Get the character set from the LC_CTYPE locale setting... 540 */ 541 542 if ((ptr = getenv("LC_CTYPE")) == NULL) 543 if ((ptr = getenv("LC_ALL")) == NULL) 544 if ((ptr = getenv("LANG")) == NULL) 545 ptr = "en_US"; 546 547 if ((csptr = strchr(ptr, '.')) != NULL) 548 { 549 /* 550 * Extract the character set from the environment... 551 */ 552 553 for (ptr = charset, csptr ++; *csptr; csptr ++) 554 if (ptr < (charset + sizeof(charset) - 1) && _cups_isalnum(*csptr)) 555 *ptr++ = *csptr; 556 557 *ptr = '\0'; 558 } 559 560 /* 561 * Get the locale for messages from the LC_MESSAGES locale setting... 562 */ 563 564 if ((ptr = getenv("LC_MESSAGES")) == NULL) 565 if ((ptr = getenv("LC_ALL")) == NULL) 566 if ((ptr = getenv("LANG")) == NULL) 567 ptr = "en_US"; 568 } 569 570 if (ptr) 571 { 572 strlcpy(locale, ptr, sizeof(locale)); 573 language = locale; 574 575 /* 576 * CUPS STR #2575: Map "nb" to "no" for back-compatibility... 577 */ 578 579 if (!strncmp(locale, "nb", 2)) 580 locale[1] = 'o'; 581 582 DEBUG_printf(("4cupsLangGet: new language value is \"%s\"", language)); 583 } 584 } 585 #endif /* __APPLE__ */ 586 587 /* 588 * If "language" is NULL at this point, then chances are we are using 589 * a language that is not installed for the base OS. 590 */ 591 592 if (!language) 593 { 594 /* 595 * Switch to the POSIX ("C") locale... 596 */ 597 598 language = "C"; 599 } 600 601 #ifdef CODESET 602 /* 603 * On systems that support the nl_langinfo(CODESET) call, use 604 * this value as the character set... 605 */ 606 607 if (!charset[0] && (csptr = nl_langinfo(CODESET)) != NULL) 608 { 609 /* 610 * Copy all of the letters and numbers in the CODESET string... 611 */ 612 613 for (ptr = charset; *csptr; csptr ++) 614 if (_cups_isalnum(*csptr) && ptr < (charset + sizeof(charset) - 1)) 615 *ptr++ = *csptr; 616 617 *ptr = '\0'; 618 619 DEBUG_printf(("4cupsLangGet: charset set to \"%s\" via " 620 "nl_langinfo(CODESET)...", charset)); 621 } 622 #endif /* CODESET */ 623 624 /* 625 * If we don't have a character set by now, default to UTF-8... 626 */ 627 628 if (!charset[0]) 629 strlcpy(charset, "UTF8", sizeof(charset)); 630 631 /* 632 * Parse the language string passed in to a locale string. "C" is the 633 * standard POSIX locale and is copied unchanged. Otherwise the 634 * language string is converted from ll-cc[.charset] (language-country) 635 * to ll_CC[.CHARSET] to match the file naming convention used by all 636 * POSIX-compliant operating systems. Invalid language names are mapped 637 * to the POSIX locale. 638 */ 639 640 country[0] = '\0'; 641 642 if (language == NULL || !language[0] || 643 !strcmp(language, "POSIX")) 644 strlcpy(langname, "C", sizeof(langname)); 645 else 646 { 647 /* 648 * Copy the parts of the locale string over safely... 649 */ 650 651 for (ptr = langname; *language; language ++) 652 if (*language == '_' || *language == '-' || *language == '.') 653 break; 654 else if (ptr < (langname + sizeof(langname) - 1)) 655 *ptr++ = (char)tolower(*language & 255); 656 657 *ptr = '\0'; 658 659 if (*language == '_' || *language == '-') 660 { 661 /* 662 * Copy the country code... 663 */ 664 665 for (language ++, ptr = country; *language; language ++) 666 if (*language == '.') 667 break; 668 else if (ptr < (country + sizeof(country) - 1)) 669 *ptr++ = (char)toupper(*language & 255); 670 671 *ptr = '\0'; 672 } 673 674 if (*language == '.' && !charset[0]) 675 { 676 /* 677 * Copy the encoding... 678 */ 679 680 for (language ++, ptr = charset; *language; language ++) 681 if (_cups_isalnum(*language) && ptr < (charset + sizeof(charset) - 1)) 682 *ptr++ = (char)toupper(*language & 255); 683 684 *ptr = '\0'; 685 } 686 687 /* 688 * Force a POSIX locale for an invalid language name... 689 */ 690 691 if (strlen(langname) != 2) 692 { 693 strlcpy(langname, "C", sizeof(langname)); 694 country[0] = '\0'; 695 charset[0] = '\0'; 696 } 697 } 698 699 DEBUG_printf(("4cupsLangGet: langname=\"%s\", country=\"%s\", charset=\"%s\"", 700 langname, country, charset)); 701 702 /* 703 * Figure out the desired encoding... 704 */ 705 706 encoding = CUPS_AUTO_ENCODING; 707 708 if (charset[0]) 709 { 710 for (i = 0; 711 i < (int)(sizeof(locale_encodings) / sizeof(locale_encodings[0])); 712 i ++) 713 if (!_cups_strcasecmp(charset, locale_encodings[i])) 714 { 715 encoding = (cups_encoding_t)i; 716 break; 717 } 718 719 if (encoding == CUPS_AUTO_ENCODING) 720 { 721 /* 722 * Map alternate names for various character sets... 723 */ 724 725 if (!_cups_strcasecmp(charset, "iso-2022-jp") || 726 !_cups_strcasecmp(charset, "sjis")) 727 encoding = CUPS_WINDOWS_932; 728 else if (!_cups_strcasecmp(charset, "iso-2022-cn")) 729 encoding = CUPS_WINDOWS_936; 730 else if (!_cups_strcasecmp(charset, "iso-2022-kr")) 731 encoding = CUPS_WINDOWS_949; 732 else if (!_cups_strcasecmp(charset, "big5")) 733 encoding = CUPS_WINDOWS_950; 734 } 735 } 736 737 DEBUG_printf(("4cupsLangGet: encoding=%d(%s)", encoding, 738 encoding == CUPS_AUTO_ENCODING ? "auto" : 739 lang_encodings[encoding])); 740 741 /* 742 * See if we already have this language/country loaded... 743 */ 744 745 if (country[0]) 746 snprintf(real, sizeof(real), "%s_%s", langname, country); 747 else 748 strlcpy(real, langname, sizeof(real)); 749 750 _cupsMutexLock(&lang_mutex); 751 752 if ((lang = cups_cache_lookup(real, encoding)) != NULL) 753 { 754 _cupsMutexUnlock(&lang_mutex); 755 756 DEBUG_printf(("3cupsLangGet: Using cached copy of \"%s\"...", real)); 757 758 return (lang); 759 } 760 761 /* 762 * See if there is a free language available; if so, use that 763 * record... 764 */ 765 766 for (lang = lang_cache; lang != NULL; lang = lang->next) 767 if (lang->used == 0) 768 break; 769 770 if (lang == NULL) 771 { 772 /* 773 * Allocate memory for the language and add it to the cache. 774 */ 775 776 if ((lang = calloc(sizeof(cups_lang_t), 1)) == NULL) 777 { 778 _cupsMutexUnlock(&lang_mutex); 779 780 return (NULL); 781 } 782 783 lang->next = lang_cache; 784 lang_cache = lang; 785 } 786 else 787 { 788 /* 789 * Free all old strings as needed... 790 */ 791 792 _cupsMessageFree(lang->strings); 793 lang->strings = NULL; 794 } 795 796 /* 797 * Then assign the language and encoding fields... 798 */ 799 800 lang->used ++; 801 strlcpy(lang->language, real, sizeof(lang->language)); 802 803 if (encoding != CUPS_AUTO_ENCODING) 804 lang->encoding = encoding; 805 else 806 lang->encoding = CUPS_UTF8; 807 808 /* 809 * Return... 810 */ 811 812 _cupsMutexUnlock(&lang_mutex); 813 814 return (lang); 815 } 816 817 818 /* 819 * '_cupsLangString()' - Get a message string. 820 * 821 * The returned string is UTF-8 encoded; use cupsUTF8ToCharset() to 822 * convert the string to the language encoding. 823 */ 824 825 const char * /* O - Localized message */ 826 _cupsLangString(cups_lang_t *lang, /* I - Language */ 827 const char *message) /* I - Message */ 828 { 829 const char *s; /* Localized message */ 830 831 /* 832 * Range check input... 833 */ 834 835 if (!lang || !message || !*message) 836 return (message); 837 838 _cupsMutexLock(&lang_mutex); 839 840 /* 841 * Load the message catalog if needed... 842 */ 843 844 if (!lang->strings) 845 cups_message_load(lang); 846 847 s = _cupsMessageLookup(lang->strings, message); 848 849 _cupsMutexUnlock(&lang_mutex); 850 851 return (s); 852 } 853 854 855 /* 856 * '_cupsMessageFree()' - Free a messages array. 857 */ 858 859 void 860 _cupsMessageFree(cups_array_t *a) /* I - Message array */ 861 { 862 #if defined(__APPLE__) && defined(CUPS_BUNDLEDIR) 863 /* 864 * Release the cups.strings dictionary as needed... 865 */ 866 867 if (cupsArrayUserData(a)) 868 CFRelease((CFDictionaryRef)cupsArrayUserData(a)); 869 #endif /* __APPLE__ && CUPS_BUNDLEDIR */ 870 871 /* 872 * Free the array... 873 */ 874 875 cupsArrayDelete(a); 876 } 877 878 879 /* 880 * '_cupsMessageLoad()' - Load a .po file into a messages array. 881 */ 882 883 cups_array_t * /* O - New message array */ 884 _cupsMessageLoad(const char *filename, /* I - Message catalog to load */ 885 int unquote) /* I - Unescape \foo in strings? */ 886 { 887 cups_file_t *fp; /* Message file */ 888 cups_array_t *a; /* Message array */ 889 _cups_message_t *m; /* Current message */ 890 char s[4096], /* String buffer */ 891 *ptr, /* Pointer into buffer */ 892 *temp; /* New string */ 893 size_t length, /* Length of combined strings */ 894 ptrlen; /* Length of string */ 895 896 897 DEBUG_printf(("4_cupsMessageLoad(filename=\"%s\")", filename)); 898 899 /* 900 * Create an array to hold the messages... 901 */ 902 903 if ((a = _cupsMessageNew(NULL)) == NULL) 904 { 905 DEBUG_puts("5_cupsMessageLoad: Unable to allocate array!"); 906 return (NULL); 907 } 908 909 /* 910 * Open the message catalog file... 911 */ 912 913 if ((fp = cupsFileOpen(filename, "r")) == NULL) 914 { 915 DEBUG_printf(("5_cupsMessageLoad: Unable to open file: %s", 916 strerror(errno))); 917 return (a); 918 } 919 920 /* 921 * Read messages from the catalog file until EOF... 922 * 923 * The format is the GNU gettext .po format, which is fairly simple: 924 * 925 * msgid "some text" 926 * msgstr "localized text" 927 * 928 * The ID and localized text can span multiple lines using the form: 929 * 930 * msgid "" 931 * "some long text" 932 * msgstr "" 933 * "localized text spanning " 934 * "multiple lines" 935 */ 936 937 m = NULL; 938 939 while (cupsFileGets(fp, s, sizeof(s)) != NULL) 940 { 941 /* 942 * Skip blank and comment lines... 943 */ 944 945 if (s[0] == '#' || !s[0]) 946 continue; 947 948 /* 949 * Strip the trailing quote... 950 */ 951 952 if ((ptr = strrchr(s, '\"')) == NULL) 953 continue; 954 955 *ptr = '\0'; 956 957 /* 958 * Find start of value... 959 */ 960 961 if ((ptr = strchr(s, '\"')) == NULL) 962 continue; 963 964 ptr ++; 965 966 /* 967 * Unquote the text... 968 */ 969 970 if (unquote) 971 cups_unquote(ptr, ptr); 972 973 /* 974 * Create or add to a message... 975 */ 976 977 if (!strncmp(s, "msgid", 5)) 978 { 979 /* 980 * Add previous message as needed... 981 */ 982 983 if (m) 984 { 985 if (m->str && m->str[0]) 986 { 987 cupsArrayAdd(a, m); 988 } 989 else 990 { 991 /* 992 * Translation is empty, don't add it... (STR #4033) 993 */ 994 995 free(m->id); 996 if (m->str) 997 free(m->str); 998 free(m); 999 } 1000 } 1001 1002 /* 1003 * Create a new message with the given msgid string... 1004 */ 1005 1006 if ((m = (_cups_message_t *)calloc(1, sizeof(_cups_message_t))) == NULL) 1007 { 1008 cupsFileClose(fp); 1009 return (a); 1010 } 1011 1012 if ((m->id = strdup(ptr)) == NULL) 1013 { 1014 free(m); 1015 cupsFileClose(fp); 1016 return (a); 1017 } 1018 } 1019 else if (s[0] == '\"' && m) 1020 { 1021 /* 1022 * Append to current string... 1023 */ 1024 1025 length = strlen(m->str ? m->str : m->id); 1026 ptrlen = strlen(ptr); 1027 1028 if ((temp = realloc(m->str ? m->str : m->id, length + ptrlen + 1)) == NULL) 1029 { 1030 if (m->str) 1031 free(m->str); 1032 free(m->id); 1033 free(m); 1034 1035 cupsFileClose(fp); 1036 return (a); 1037 } 1038 1039 if (m->str) 1040 { 1041 /* 1042 * Copy the new portion to the end of the msgstr string - safe 1043 * to use memcpy because the buffer is allocated to the correct 1044 * size... 1045 */ 1046 1047 m->str = temp; 1048 1049 memcpy(m->str + length, ptr, ptrlen + 1); 1050 } 1051 else 1052 { 1053 /* 1054 * Copy the new portion to the end of the msgid string - safe 1055 * to use memcpy because the buffer is allocated to the correct 1056 * size... 1057 */ 1058 1059 m->id = temp; 1060 1061 memcpy(m->id + length, ptr, ptrlen + 1); 1062 } 1063 } 1064 else if (!strncmp(s, "msgstr", 6) && m) 1065 { 1066 /* 1067 * Set the string... 1068 */ 1069 1070 if ((m->str = strdup(ptr)) == NULL) 1071 { 1072 free(m->id); 1073 free(m); 1074 1075 cupsFileClose(fp); 1076 return (a); 1077 } 1078 } 1079 } 1080 1081 /* 1082 * Add the last message string to the array as needed... 1083 */ 1084 1085 if (m) 1086 { 1087 if (m->str && m->str[0]) 1088 { 1089 cupsArrayAdd(a, m); 1090 } 1091 else 1092 { 1093 /* 1094 * Translation is empty, don't add it... (STR #4033) 1095 */ 1096 1097 free(m->id); 1098 if (m->str) 1099 free(m->str); 1100 free(m); 1101 } 1102 } 1103 1104 /* 1105 * Close the message catalog file and return the new array... 1106 */ 1107 1108 cupsFileClose(fp); 1109 1110 DEBUG_printf(("5_cupsMessageLoad: Returning %d messages...", 1111 cupsArrayCount(a))); 1112 1113 return (a); 1114 } 1115 1116 1117 /* 1118 * '_cupsMessageLookup()' - Lookup a message string. 1119 */ 1120 1121 const char * /* O - Localized message */ 1122 _cupsMessageLookup(cups_array_t *a, /* I - Message array */ 1123 const char *m) /* I - Message */ 1124 { 1125 _cups_message_t key, /* Search key */ 1126 *match; /* Matching message */ 1127 1128 1129 /* 1130 * Lookup the message string; if it doesn't exist in the catalog, 1131 * then return the message that was passed to us... 1132 */ 1133 1134 key.id = (char *)m; 1135 match = (_cups_message_t *)cupsArrayFind(a, &key); 1136 1137 #if defined(__APPLE__) && defined(CUPS_BUNDLEDIR) 1138 if (!match && cupsArrayUserData(a)) 1139 { 1140 /* 1141 * Try looking the string up in the cups.strings dictionary... 1142 */ 1143 1144 CFDictionaryRef dict; /* cups.strings dictionary */ 1145 CFStringRef cfm, /* Message as a CF string */ 1146 cfstr; /* Localized text as a CF string */ 1147 1148 dict = (CFDictionaryRef)cupsArrayUserData(a); 1149 cfm = CFStringCreateWithCString(kCFAllocatorDefault, m, 1150 kCFStringEncodingUTF8); 1151 match = calloc(1, sizeof(_cups_message_t)); 1152 match->id = strdup(m); 1153 cfstr = cfm ? CFDictionaryGetValue(dict, cfm) : NULL; 1154 1155 if (cfstr) 1156 { 1157 char buffer[1024]; /* Message buffer */ 1158 1159 CFStringGetCString(cfstr, buffer, sizeof(buffer), kCFStringEncodingUTF8); 1160 match->str = strdup(buffer); 1161 1162 DEBUG_printf(("1_cupsMessageLookup: Found \"%s\" as \"%s\"...", 1163 m, buffer)); 1164 } 1165 else 1166 { 1167 match->str = strdup(m); 1168 1169 DEBUG_printf(("1_cupsMessageLookup: Did not find \"%s\"...", m)); 1170 } 1171 1172 cupsArrayAdd(a, match); 1173 1174 if (cfm) 1175 CFRelease(cfm); 1176 } 1177 #endif /* __APPLE__ && CUPS_BUNDLEDIR */ 1178 1179 if (match && match->str) 1180 return (match->str); 1181 else 1182 return (m); 1183 } 1184 1185 1186 /* 1187 * '_cupsMessageNew()' - Make a new message catalog array. 1188 */ 1189 1190 cups_array_t * /* O - Array */ 1191 _cupsMessageNew(void *context) /* I - User data */ 1192 { 1193 return (cupsArrayNew3((cups_array_func_t)cups_message_compare, context, 1194 (cups_ahash_func_t)NULL, 0, 1195 (cups_acopy_func_t)NULL, 1196 (cups_afree_func_t)cups_message_free)); 1197 } 1198 1199 1200 #ifdef __APPLE__ 1201 /* 1202 * 'appleLangDefault()' - Get the default locale string. 1203 */ 1204 1205 static const char * /* O - Locale string */ 1206 appleLangDefault(void) 1207 { 1208 CFBundleRef bundle; /* Main bundle (if any) */ 1209 CFArrayRef bundleList; /* List of localizations in bundle */ 1210 CFPropertyListRef localizationList = NULL; 1211 /* List of localization data */ 1212 CFStringRef languageName; /* Current name */ 1213 char *lang; /* LANG environment variable */ 1214 _cups_globals_t *cg = _cupsGlobals(); 1215 /* Pointer to library globals */ 1216 1217 1218 DEBUG_puts("2appleLangDefault()"); 1219 1220 /* 1221 * Only do the lookup and translation the first time. 1222 */ 1223 1224 if (!cg->language[0]) 1225 { 1226 if (getenv("SOFTWARE") != NULL && (lang = getenv("LANG")) != NULL) 1227 { 1228 DEBUG_printf(("3appleLangDefault: Using LANG=%s", lang)); 1229 strlcpy(cg->language, lang, sizeof(cg->language)); 1230 return (cg->language); 1231 } 1232 else if ((bundle = CFBundleGetMainBundle()) != NULL && 1233 (bundleList = CFBundleCopyBundleLocalizations(bundle)) != NULL) 1234 { 1235 CFURLRef resources = CFBundleCopyResourcesDirectoryURL(bundle); 1236 1237 DEBUG_puts("3appleLangDefault: Getting localizationList from bundle."); 1238 1239 if (resources) 1240 { 1241 CFStringRef cfpath = CFURLCopyPath(resources); 1242 char path[1024]; 1243 1244 if (cfpath) 1245 { 1246 /* 1247 * See if we have an Info.plist file in the bundle... 1248 */ 1249 1250 CFStringGetCString(cfpath, path, sizeof(path), kCFStringEncodingUTF8); 1251 DEBUG_printf(("3appleLangDefault: Got a resource URL (\"%s\")", path)); 1252 strlcat(path, "Contents/Info.plist", sizeof(path)); 1253 1254 if (!access(path, R_OK)) 1255 localizationList = CFBundleCopyPreferredLocalizationsFromArray(bundleList); 1256 else 1257 DEBUG_puts("3appleLangDefault: No Info.plist, ignoring resource URL..."); 1258 1259 CFRelease(cfpath); 1260 } 1261 1262 CFRelease(resources); 1263 } 1264 else 1265 DEBUG_puts("3appleLangDefault: No resource URL."); 1266 1267 CFRelease(bundleList); 1268 } 1269 1270 if (!localizationList) 1271 { 1272 DEBUG_puts("3appleLangDefault: Getting localizationList from preferences."); 1273 1274 localizationList = 1275 CFPreferencesCopyAppValue(CFSTR("AppleLanguages"), 1276 kCFPreferencesCurrentApplication); 1277 } 1278 1279 if (localizationList) 1280 { 1281 #ifdef DEBUG 1282 if (CFGetTypeID(localizationList) == CFArrayGetTypeID()) 1283 DEBUG_printf(("3appleLangDefault: Got localizationList, %d entries.", 1284 (int)CFArrayGetCount(localizationList))); 1285 else 1286 DEBUG_puts("3appleLangDefault: Got localizationList but not an array."); 1287 #endif /* DEBUG */ 1288 1289 if (CFGetTypeID(localizationList) == CFArrayGetTypeID() && 1290 CFArrayGetCount(localizationList) > 0) 1291 { 1292 languageName = CFArrayGetValueAtIndex(localizationList, 0); 1293 1294 if (languageName && 1295 CFGetTypeID(languageName) == CFStringGetTypeID()) 1296 { 1297 if (_cupsAppleLocale(languageName, cg->language, sizeof(cg->language))) 1298 DEBUG_printf(("3appleLangDefault: cg->language=\"%s\"", 1299 cg->language)); 1300 else 1301 DEBUG_puts("3appleLangDefault: Unable to get locale."); 1302 } 1303 } 1304 1305 CFRelease(localizationList); 1306 } 1307 1308 /* 1309 * If we didn't find the language, default to en_US... 1310 */ 1311 1312 if (!cg->language[0]) 1313 { 1314 DEBUG_puts("3appleLangDefault: Defaulting to en_US."); 1315 strlcpy(cg->language, "en_US.UTF-8", sizeof(cg->language)); 1316 } 1317 } 1318 else 1319 DEBUG_printf(("3appleLangDefault: Using previous locale \"%s\".", cg->language)); 1320 1321 /* 1322 * Return the cached locale... 1323 */ 1324 1325 return (cg->language); 1326 } 1327 1328 1329 # ifdef CUPS_BUNDLEDIR 1330 /* 1331 * 'appleMessageLoad()' - Load a message catalog from a localizable bundle. 1332 */ 1333 1334 static cups_array_t * /* O - Message catalog */ 1335 appleMessageLoad(const char *locale) /* I - Locale ID */ 1336 { 1337 char filename[1024], /* Path to cups.strings file */ 1338 applelang[256], /* Apple language ID */ 1339 baselang[3]; /* Base language */ 1340 CFURLRef url; /* URL to cups.strings file */ 1341 CFReadStreamRef stream = NULL; /* File stream */ 1342 CFPropertyListRef plist = NULL; /* Localization file */ 1343 #ifdef DEBUG 1344 CFErrorRef error = NULL; /* Error when opening file */ 1345 #endif /* DEBUG */ 1346 1347 1348 DEBUG_printf(("appleMessageLoad(locale=\"%s\")", locale)); 1349 1350 /* 1351 * Load the cups.strings file... 1352 */ 1353 1354 snprintf(filename, sizeof(filename), 1355 CUPS_BUNDLEDIR "/Resources/%s.lproj/cups.strings", 1356 _cupsAppleLanguage(locale, applelang, sizeof(applelang))); 1357 1358 if (access(filename, 0)) 1359 { 1360 /* 1361 * <rdar://problem/22086642> 1362 * 1363 * Try with original locale string... 1364 */ 1365 1366 snprintf(filename, sizeof(filename), CUPS_BUNDLEDIR "/Resources/%s.lproj/cups.strings", locale); 1367 } 1368 1369 if (access(filename, 0)) 1370 { 1371 /* 1372 * <rdar://problem/25292403> 1373 * 1374 * Try with just the language code... 1375 */ 1376 1377 strlcpy(baselang, locale, sizeof(baselang)); 1378 snprintf(filename, sizeof(filename), CUPS_BUNDLEDIR "/Resources/%s.lproj/cups.strings", baselang); 1379 } 1380 1381 DEBUG_printf(("1appleMessageLoad: filename=\"%s\"", filename)); 1382 1383 if (access(filename, 0)) 1384 { 1385 /* 1386 * Try alternate lproj directory names... 1387 */ 1388 1389 if (!strncmp(locale, "en", 2)) 1390 locale = "English"; 1391 else if (!strncmp(locale, "nb", 2)) 1392 locale = "no"; 1393 else if (!strncmp(locale, "nl", 2)) 1394 locale = "Dutch"; 1395 else if (!strncmp(locale, "fr", 2)) 1396 locale = "French"; 1397 else if (!strncmp(locale, "de", 2)) 1398 locale = "German"; 1399 else if (!strncmp(locale, "it", 2)) 1400 locale = "Italian"; 1401 else if (!strncmp(locale, "ja", 2)) 1402 locale = "Japanese"; 1403 else if (!strncmp(locale, "es", 2)) 1404 locale = "Spanish"; 1405 else if (!strcmp(locale, "zh_HK") || !strncmp(locale, "zh-Hant", 7)) 1406 { 1407 /* 1408 * <rdar://problem/22130168> 1409 * <rdar://problem/27245567> 1410 * 1411 * Try zh_TW first, then zh... Sigh... 1412 */ 1413 1414 if (!access(CUPS_BUNDLEDIR "/Resources/zh_TW.lproj/cups.strings", 0)) 1415 locale = "zh_TW"; 1416 else 1417 locale = "zh"; 1418 } 1419 else if (strstr(locale, "_") != NULL || strstr(locale, "-") != NULL) 1420 { 1421 /* 1422 * Drop country code, just try language... 1423 */ 1424 1425 strlcpy(baselang, locale, sizeof(baselang)); 1426 locale = baselang; 1427 } 1428 1429 snprintf(filename, sizeof(filename), 1430 CUPS_BUNDLEDIR "/Resources/%s.lproj/cups.strings", locale); 1431 DEBUG_printf(("1appleMessageLoad: alternate filename=\"%s\"", filename)); 1432 } 1433 1434 url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, 1435 (UInt8 *)filename, 1436 (CFIndex)strlen(filename), false); 1437 if (url) 1438 { 1439 stream = CFReadStreamCreateWithFile(kCFAllocatorDefault, url); 1440 if (stream) 1441 { 1442 /* 1443 * Read the property list containing the localization data. 1444 * 1445 * NOTE: This code currently generates a clang "potential leak" 1446 * warning, but the object is released in _cupsMessageFree(). 1447 */ 1448 1449 CFReadStreamOpen(stream); 1450 1451 #ifdef DEBUG 1452 plist = CFPropertyListCreateWithStream(kCFAllocatorDefault, stream, 0, 1453 kCFPropertyListImmutable, NULL, 1454 &error); 1455 if (error) 1456 { 1457 CFStringRef msg = CFErrorCopyDescription(error); 1458 /* Error message */ 1459 1460 CFStringGetCString(msg, filename, sizeof(filename), 1461 kCFStringEncodingUTF8); 1462 DEBUG_printf(("1appleMessageLoad: %s", filename)); 1463 1464 CFRelease(msg); 1465 CFRelease(error); 1466 } 1467 1468 #else 1469 plist = CFPropertyListCreateWithStream(kCFAllocatorDefault, stream, 0, 1470 kCFPropertyListImmutable, NULL, 1471 NULL); 1472 #endif /* DEBUG */ 1473 1474 if (plist && CFGetTypeID(plist) != CFDictionaryGetTypeID()) 1475 { 1476 CFRelease(plist); 1477 plist = NULL; 1478 } 1479 1480 CFRelease(stream); 1481 } 1482 1483 CFRelease(url); 1484 } 1485 1486 DEBUG_printf(("1appleMessageLoad: url=%p, stream=%p, plist=%p", url, stream, 1487 plist)); 1488 1489 /* 1490 * Create and return an empty array to act as a cache for messages, passing the 1491 * plist as the user data. 1492 */ 1493 1494 return (_cupsMessageNew((void *)plist)); 1495 } 1496 # endif /* CUPS_BUNDLEDIR */ 1497 #endif /* __APPLE__ */ 1498 1499 1500 /* 1501 * 'cups_cache_lookup()' - Lookup a language in the cache... 1502 */ 1503 1504 static cups_lang_t * /* O - Language data or NULL */ 1505 cups_cache_lookup( 1506 const char *name, /* I - Name of locale */ 1507 cups_encoding_t encoding) /* I - Encoding of locale */ 1508 { 1509 cups_lang_t *lang; /* Current language */ 1510 1511 1512 DEBUG_printf(("7cups_cache_lookup(name=\"%s\", encoding=%d(%s))", name, 1513 encoding, encoding == CUPS_AUTO_ENCODING ? "auto" : 1514 lang_encodings[encoding])); 1515 1516 /* 1517 * Loop through the cache and return a match if found... 1518 */ 1519 1520 for (lang = lang_cache; lang != NULL; lang = lang->next) 1521 { 1522 DEBUG_printf(("9cups_cache_lookup: lang=%p, language=\"%s\", " 1523 "encoding=%d(%s)", (void *)lang, lang->language, lang->encoding, 1524 lang_encodings[lang->encoding])); 1525 1526 if (!strcmp(lang->language, name) && 1527 (encoding == CUPS_AUTO_ENCODING || encoding == lang->encoding)) 1528 { 1529 lang->used ++; 1530 1531 DEBUG_puts("8cups_cache_lookup: returning match!"); 1532 1533 return (lang); 1534 } 1535 } 1536 1537 DEBUG_puts("8cups_cache_lookup: returning NULL!"); 1538 1539 return (NULL); 1540 } 1541 1542 1543 /* 1544 * 'cups_message_compare()' - Compare two messages. 1545 */ 1546 1547 static int /* O - Result of comparison */ 1548 cups_message_compare( 1549 _cups_message_t *m1, /* I - First message */ 1550 _cups_message_t *m2) /* I - Second message */ 1551 { 1552 return (strcmp(m1->id, m2->id)); 1553 } 1554 1555 1556 /* 1557 * 'cups_message_free()' - Free a message. 1558 */ 1559 1560 static void 1561 cups_message_free(_cups_message_t *m) /* I - Message */ 1562 { 1563 if (m->id) 1564 free(m->id); 1565 1566 if (m->str) 1567 free(m->str); 1568 1569 free(m); 1570 } 1571 1572 1573 /* 1574 * 'cups_message_load()' - Load the message catalog for a language. 1575 */ 1576 1577 static void 1578 cups_message_load(cups_lang_t *lang) /* I - Language */ 1579 { 1580 #if defined(__APPLE__) && defined(CUPS_BUNDLEDIR) 1581 lang->strings = appleMessageLoad(lang->language); 1582 1583 #else 1584 char filename[1024]; /* Filename for language locale file */ 1585 _cups_globals_t *cg = _cupsGlobals(); 1586 /* Pointer to library globals */ 1587 1588 1589 snprintf(filename, sizeof(filename), "%s/%s/cups_%s.po", cg->localedir, 1590 lang->language, lang->language); 1591 1592 if (strchr(lang->language, '_') && access(filename, 0)) 1593 { 1594 /* 1595 * Country localization not available, look for generic localization... 1596 */ 1597 1598 snprintf(filename, sizeof(filename), "%s/%.2s/cups_%.2s.po", cg->localedir, 1599 lang->language, lang->language); 1600 1601 if (access(filename, 0)) 1602 { 1603 /* 1604 * No generic localization, so use POSIX... 1605 */ 1606 1607 DEBUG_printf(("4cups_message_load: access(\"%s\", 0): %s", filename, 1608 strerror(errno))); 1609 1610 snprintf(filename, sizeof(filename), "%s/C/cups_C.po", cg->localedir); 1611 } 1612 } 1613 1614 /* 1615 * Read the strings from the file... 1616 */ 1617 1618 lang->strings = _cupsMessageLoad(filename, 1); 1619 #endif /* __APPLE__ && CUPS_BUNDLEDIR */ 1620 } 1621 1622 1623 /* 1624 * 'cups_unquote()' - Unquote characters in strings... 1625 */ 1626 1627 static void 1628 cups_unquote(char *d, /* O - Unquoted string */ 1629 const char *s) /* I - Original string */ 1630 { 1631 while (*s) 1632 { 1633 if (*s == '\\') 1634 { 1635 s ++; 1636 if (isdigit(*s)) 1637 { 1638 *d = 0; 1639 1640 while (isdigit(*s)) 1641 { 1642 *d = *d * 8 + *s - '0'; 1643 s ++; 1644 } 1645 1646 d ++; 1647 } 1648 else 1649 { 1650 if (*s == 'n') 1651 *d ++ = '\n'; 1652 else if (*s == 'r') 1653 *d ++ = '\r'; 1654 else if (*s == 't') 1655 *d ++ = '\t'; 1656 else 1657 *d++ = *s; 1658 1659 s ++; 1660 } 1661 } 1662 else 1663 *d++ = *s++; 1664 } 1665 1666 *d = '\0'; 1667 } 1668