3 * Copyright (C) 2002,2003 A.J. van Os; Released under GNU GPL
6 * Read the summary information of a Word document
13 #define P_HEADER_SZ 28
14 #define P_SECTIONLIST_SZ 20
16 #define P_SECTION_MAX_SZ (2 * P_SECTIONLIST_SZ + P_LENGTH_SZ)
17 #define P_SECTION_SZ(x) ((x) * P_SECTIONLIST_SZ + P_LENGTH_SZ)
22 #define PID_CREATE_DTM 12
23 #define PID_LASTSAVE_DTM 13
24 #define PID_APPNAME 18
26 #define PIDD_MANAGER 14
27 #define PIDD_COMPANY 15
30 #define VT_FILETIME 64
32 #define TIME_OFFSET_HI 0x019db1de
33 #define TIME_OFFSET_LO 0xd53e8000
35 static char *szTitle
= NULL
;
36 static char *szSubject
= NULL
;
37 static char *szAuthor
= NULL
;
38 static time_t tCreateDtm
= (time_t)-1;
39 static time_t tLastSaveDtm
= (time_t)-1;
40 static char *szAppName
= NULL
;
41 static char *szManager
= NULL
;
42 static char *szCompany
= NULL
;
43 static USHORT usLid
= (USHORT
)-1;
47 * vDestroySummaryInfo - destroy the summary information
50 vDestroySummaryInfo(void)
52 DBG_MSG("vDestroySummaryInfo");
54 szTitle
= xfree(szTitle
);
55 szSubject
= xfree(szSubject
);
56 szAuthor
= xfree(szAuthor
);
57 tCreateDtm
= (time_t)-1;
58 tLastSaveDtm
= (time_t)-1;
59 szAppName
= xfree(szAppName
);
60 szManager
= xfree(szManager
);
61 szCompany
= xfree(szCompany
);
63 } /* end of vDestroySummaryInfo */
66 * tConvertDosDate - convert DOS date format
68 * returns Unix time_t or -1
71 tConvertDosDate(const char *szDosDate
)
77 memset(&tTime
, 0, sizeof(tTime
));
80 if (!isdigit(*pcTmp
)) {
83 tTime
.tm_mon
= (int)(*pcTmp
- '0');
85 if (isdigit(*pcTmp
)) {
87 tTime
.tm_mon
+= (int)(*pcTmp
- '0');
90 /* Get the first separater */
91 if (isalnum(*pcTmp
)) {
96 if (!isdigit(*pcTmp
)) {
99 tTime
.tm_mday
= (int)(*pcTmp
- '0');
101 if (isdigit(*pcTmp
)) {
103 tTime
.tm_mday
+= (int)(*pcTmp
- '0');
106 /* Get the second separater */
107 if (isalnum(*pcTmp
)) {
112 if (!isdigit(*pcTmp
)) {
115 tTime
.tm_year
= (int)(*pcTmp
- '0');
117 if (isdigit(*pcTmp
)) {
119 tTime
.tm_year
+= (int)(*pcTmp
- '0');
122 /* Check the values */
123 if (tTime
.tm_mon
== 0 || tTime
.tm_mday
== 0 || tTime
.tm_mday
> 31) {
126 /* Correct the values */
127 tTime
.tm_mon
--; /* From 01-12 to 00-11 */
128 if (tTime
.tm_year
< 80) {
129 tTime
.tm_year
+= 100; /* 00 means 2000 is 100 */
132 tResult
= mktime(&tTime
);
133 NO_DBG_MSG(ctime(&tResult
));
135 } /* end of tConvertDosDate */
138 * tConvertDTTM - convert Windows Date and Time format
140 * returns Unix time_t or -1
143 tConvertDTTM(ULONG ulDTTM
)
151 memset(&tTime
, 0, sizeof(tTime
));
152 tTime
.tm_min
= (int)(ulDTTM
& 0x0000003f);
153 tTime
.tm_hour
= (int)((ulDTTM
& 0x000007c0) >> 6);
154 tTime
.tm_mday
= (int)((ulDTTM
& 0x0000f800) >> 11);
155 tTime
.tm_mon
= (int)((ulDTTM
& 0x000f0000) >> 16);
156 tTime
.tm_year
= (int)((ulDTTM
& 0x1ff00000) >> 20);
158 tTime
.tm_mon
--; /* From 01-12 to 00-11 */
159 tResult
= mktime(&tTime
);
160 NO_DBG_MSG(ctime(&tResult
));
162 } /* end of tConvertDTTM */
165 * szLpstr - get a zero terminate string property
168 szLpstr(ULONG ulOffset
, const UCHAR
*aucBuffer
)
170 char *szStart
, *szResult
, *szTmp
;
173 tTmp
= (size_t)ulGetLong(ulOffset
+ 4, aucBuffer
);
175 NO_DBG_MSG(aucBuffer
+ ulOffset
+ 8);
176 /* Remove white space from the start of the string */
177 szStart
= (char *)aucBuffer
+ ulOffset
+ 8;
178 fail(strlen(szStart
) >= tTmp
);
179 while (isspace(*szStart
)) {
182 if (szStart
[0] == '\0') {
185 szResult
= xstrdup(szStart
);
186 /* Remove white space from the end of the string */
187 szTmp
= szResult
+ strlen(szResult
) - 1;
188 while (isspace(*szTmp
)) {
192 NO_DBG_MSG(szResult
);
194 } /* end of szLpstr */
197 * tFiletime - get a filetime property
200 tFiletime(ULONG ulOffset
, const UCHAR
*aucBuffer
)
202 double dHi
, dLo
, dTmp
;
206 ulLo
= ulGetLong(ulOffset
+ 4, aucBuffer
);
207 ulHi
= ulGetLong(ulOffset
+ 8, aucBuffer
);
211 /* Move the starting point from 01 Jan 1601 to 01 Jan 1970 */
212 dHi
= (double)ulHi
- (double)TIME_OFFSET_HI
;
213 dLo
= (double)ulLo
- (double)TIME_OFFSET_LO
;
217 /* Combine the values and divide by 10^7 to get seconds */
218 dTmp
= dLo
/ 10000000.0; /* 10^7 */
219 dTmp
+= dHi
* 429.4967926; /* 2^32 / 10^7 */
223 if (dTmp
- 0.5 < TIME_T_MIN
|| dTmp
+ 0.5 > TIME_T_MAX
) {
226 tResult
= dTmp
< 0.0 ? (time_t)(dTmp
- 0.5) : (time_t)(dTmp
+ 0.5);
227 NO_DBG_MSG(ctime(&tResult
));
229 } /* end of tFiletime */
232 * vAnalyseSummaryInfo -
235 vAnalyseSummaryInfo(const UCHAR
*aucBuffer
)
238 size_t tIndex
, tCount
, tPropID
, tPropType
;
240 tCount
= (size_t)ulGetLong(4, aucBuffer
);
242 for (tIndex
= 0; tIndex
< tCount
; tIndex
++) {
243 tPropID
= (size_t)ulGetLong(8 + tIndex
* 8, aucBuffer
);
244 ulOffset
= ulGetLong(12 + tIndex
* 8, aucBuffer
);
246 NO_DBG_HEX(ulOffset
);
247 tPropType
= (size_t)ulGetLong(ulOffset
, aucBuffer
);
248 NO_DBG_DEC(tPropType
);
251 if (tPropType
== VT_LPSTR
&& szTitle
== NULL
) {
252 szTitle
= szLpstr(ulOffset
, aucBuffer
);
256 if (tPropType
== VT_LPSTR
&& szSubject
== NULL
) {
257 szSubject
= szLpstr(ulOffset
, aucBuffer
);
261 if (tPropType
== VT_LPSTR
&& szAuthor
== NULL
) {
262 szAuthor
= szLpstr(ulOffset
, aucBuffer
);
266 if (tPropType
== VT_FILETIME
&&
267 tCreateDtm
== (time_t)-1) {
268 tCreateDtm
= tFiletime(ulOffset
, aucBuffer
);
271 case PID_LASTSAVE_DTM
:
272 if (tPropType
== VT_FILETIME
&&
273 tLastSaveDtm
== (time_t)-1) {
274 tLastSaveDtm
= tFiletime(ulOffset
, aucBuffer
);
278 if (tPropType
== VT_LPSTR
&& szAppName
== NULL
) {
279 szAppName
= szLpstr(ulOffset
, aucBuffer
);
286 } /* end of vAnalyseSummaryInfo */
289 * vAnalyseDocumentSummaryInfo -
292 vAnalyseDocumentSummaryInfo(const UCHAR
*aucBuffer
)
295 size_t tIndex
, tCount
, tPropID
, tPropType
;
297 tCount
= (size_t)ulGetLong(4, aucBuffer
);
299 for (tIndex
= 0; tIndex
< tCount
; tIndex
++) {
300 tPropID
= (size_t)ulGetLong(8 + tIndex
* 8, aucBuffer
);
301 ulOffset
= ulGetLong(12 + tIndex
* 8, aucBuffer
);
303 NO_DBG_HEX(ulOffset
);
304 tPropType
= (size_t)ulGetLong(ulOffset
, aucBuffer
);
305 NO_DBG_DEC(tPropType
);
308 if (tPropType
== VT_LPSTR
&& szManager
== NULL
) {
309 szManager
= szLpstr(ulOffset
, aucBuffer
);
313 if (tPropType
== VT_LPSTR
&& szCompany
== NULL
) {
314 szCompany
= szLpstr(ulOffset
, aucBuffer
);
321 } /* end of vAnalyseDocumentSummaryInfo */
324 * pucAnalyseSummaryInfoHeader-
327 pucAnalyseSummaryInfoHeader(FILE *pFile
,
328 ULONG ulStartBlock
, ULONG ulSize
,
329 const ULONG
*aulBBD
, size_t tBBDLen
,
330 const ULONG
*aulSBD
, size_t tSBDLen
)
332 const ULONG
*aulBlockDepot
;
334 size_t tBlockDepotLen
, tBlockSize
, tSectionCount
, tLength
;
335 ULONG ulTmp
, ulOffset
;
336 USHORT usLittleEndian
, usEmpty
, usOS
, usVersion
;
337 UCHAR aucHdr
[P_HEADER_SZ
], aucSecLst
[P_SECTION_MAX_SZ
];
339 if (ulSize
< MIN_SIZE_FOR_BBD_USE
) {
340 /* Use the Small Block Depot */
341 aulBlockDepot
= aulSBD
;
342 tBlockDepotLen
= tSBDLen
;
343 tBlockSize
= SMALL_BLOCK_SIZE
;
345 /* Use the Big Block Depot */
346 aulBlockDepot
= aulBBD
;
347 tBlockDepotLen
= tBBDLen
;
348 tBlockSize
= BIG_BLOCK_SIZE
;
351 if (tBlockDepotLen
== 0) {
352 DBG_MSG("The Block Depot length is zero");
356 /* Read the Summery Information header */
357 if (!bReadBuffer(pFile
, ulStartBlock
,
358 aulBlockDepot
, tBlockDepotLen
, tBlockSize
,
359 aucHdr
, 0, P_HEADER_SZ
)) {
362 NO_DBG_PRINT_BLOCK(aucHdr
, P_HEADER_SZ
);
364 /* Analyse the Summery Information header */
365 usLittleEndian
= usGetWord(0, aucHdr
);
366 if (usLittleEndian
!= 0xfffe) {
367 DBG_HEX(usLittleEndian
);
368 DBG_MSG_C(usLittleEndian
== 0xfeff, "Big endian");
371 usEmpty
= usGetWord(2, aucHdr
);
372 if (usEmpty
!= 0x0000) {
376 ulTmp
= ulGetLong(4, aucHdr
);
378 usOS
= (USHORT
)(ulTmp
>> 16);
379 usVersion
= (USHORT
)(ulTmp
& 0xffff);
398 tSectionCount
= (size_t)ulGetLong(24, aucHdr
);
399 DBG_DEC_C(tSectionCount
!= 1 && tSectionCount
!= 2, tSectionCount
);
400 if (tSectionCount
!= 1 && tSectionCount
!= 2) {
404 /* Read the Summery Information Section Lists */
405 if (!bReadBuffer(pFile
, ulStartBlock
,
406 aulBlockDepot
, tBlockDepotLen
, tBlockSize
,
407 aucSecLst
, P_HEADER_SZ
, P_SECTION_SZ(tSectionCount
))) {
410 NO_DBG_PRINT_BLOCK(aucSecLst
, P_SECTION_SZ(tSectionCount
));
412 ulTmp
= ulGetLong(0, aucSecLst
);
414 ulTmp
= ulGetLong(4, aucSecLst
);
416 ulTmp
= ulGetLong(8, aucSecLst
);
418 ulTmp
= ulGetLong(12, aucSecLst
);
420 ulOffset
= ulGetLong(16, aucSecLst
);
421 DBG_DEC_C(ulOffset
!= P_HEADER_SZ
+ P_SECTIONLIST_SZ
&&
422 ulOffset
!= P_HEADER_SZ
+ 2 * P_SECTIONLIST_SZ
,
424 fail(ulOffset
!= P_HEADER_SZ
+ P_SECTIONLIST_SZ
&&
425 ulOffset
!= P_HEADER_SZ
+ 2 * P_SECTIONLIST_SZ
);
427 (size_t)ulGetLong(tSectionCount
* P_SECTIONLIST_SZ
, aucSecLst
);
429 fail(ulOffset
+ tLength
> ulSize
);
431 /* Read the Summery Information */
432 aucBuffer
= xmalloc(tLength
);
433 if (!bReadBuffer(pFile
, ulStartBlock
,
434 aulBlockDepot
, tBlockDepotLen
, tBlockSize
,
435 aucBuffer
, ulOffset
, tLength
)) {
436 aucBuffer
= xfree(aucBuffer
);
439 NO_DBG_PRINT_BLOCK(aucBuffer
, tLength
);
441 } /* end of pucAnalyseSummaryInfoHeader */
444 * vSet0SummaryInfo - set summary information from a Word for DOS file
447 vSet0SummaryInfo(FILE *pFile
, const UCHAR
*aucHeader
)
450 ULONG ulBeginSumdInfo
, ulBeginNextBlock
;
452 USHORT usCodepage
, usOffset
;
454 fail(pFile
== NULL
|| aucHeader
== NULL
);
456 /* First check the header */
457 usCodepage
= usGetWord(0x7e, aucHeader
);
459 switch (usCodepage
) {
460 case 850: usLid
= 0x0809; break; /* Latin1 -> British English */
461 case 862: usLid
= 0x040d; break; /* Hebrew */
462 case 866: usLid
= 0x0419; break; /* Russian */
465 default: usLid
= 0x0409; break; /* ASCII -> American English */
468 /* Second check the summary information block */
469 ulBeginSumdInfo
= 128 * (ULONG
)usGetWord(0x1c, aucHeader
);
470 DBG_HEX(ulBeginSumdInfo
);
471 ulBeginNextBlock
= 128 * (ULONG
)usGetWord(0x6a, aucHeader
);
472 DBG_HEX(ulBeginNextBlock
);
474 if (ulBeginSumdInfo
>= ulBeginNextBlock
|| ulBeginNextBlock
== 0) {
475 /* There is no summary information block */
478 tLen
= (size_t)(ulBeginNextBlock
- ulBeginSumdInfo
);
479 aucBuffer
= xmalloc(tLen
);
480 /* Read the summary information block */
481 if (!bReadBytes(aucBuffer
, tLen
, ulBeginSumdInfo
, pFile
)) {
484 usOffset
= usGetWord(0, aucBuffer
);
485 if (aucBuffer
[usOffset
] != 0) {
486 NO_DBG_MSG(aucBuffer
+ usOffset
);
487 szTitle
= xstrdup((char *)aucBuffer
+ usOffset
);
489 usOffset
= usGetWord(2, aucBuffer
);
490 if (aucBuffer
[usOffset
] != 0) {
491 NO_DBG_MSG(aucBuffer
+ usOffset
);
492 szAuthor
= xstrdup((char *)aucBuffer
+ usOffset
);
494 usOffset
= usGetWord(12, aucBuffer
);
495 if (aucBuffer
[usOffset
] != 0) {
496 NO_DBG_STRN(aucBuffer
+ usOffset
, 8);
497 tLastSaveDtm
= tConvertDosDate((char *)aucBuffer
+ usOffset
);
499 usOffset
= usGetWord(14, aucBuffer
);
500 if (aucBuffer
[usOffset
] != 0) {
501 NO_DBG_STRN(aucBuffer
+ usOffset
, 8);
502 tCreateDtm
= tConvertDosDate((char *)aucBuffer
+ usOffset
);
504 aucBuffer
= xfree(aucBuffer
);
505 } /* end of vSet0SummaryInfo */
508 * vSet2SummaryInfo - set summary information from a WinWord 1/2 file
511 vSet2SummaryInfo(FILE *pFile
, int iWordVersion
, const UCHAR
*aucHeader
)
514 ULONG ulBeginSumdInfo
, ulBeginDocpInfo
, ulTmp
;
515 size_t tSumdInfoLen
, tDocpInfoLen
, tLen
, tCounter
, tStart
;
517 fail(pFile
== NULL
|| aucHeader
== NULL
);
518 fail(iWordVersion
!= 1 && iWordVersion
!= 2);
520 /* First check the header */
521 usLid
= usGetWord(0x06, aucHeader
); /* Language IDentification */
523 if (usLid
< 999 && iWordVersion
== 1) {
525 case 1: usLid
= 0x0409; break; /* American English */
526 case 2: usLid
= 0x0c0c; break; /* Canadian French */
527 case 31: usLid
= 0x0413; break; /* Dutch */
528 case 33: usLid
= 0x040c; break; /* French */
529 case 34: usLid
= 0x040a; break; /* Spanish */
530 case 44: usLid
= 0x0809; break; /* British English */
531 case 49: usLid
= 0x0407; break; /* German */
535 usLid
= 0x0409; /* American English */
540 /* Second check the associated strings */
541 ulBeginSumdInfo
= ulGetLong(0x118, aucHeader
); /* fcSttbfAssoc */
542 DBG_HEX(ulBeginSumdInfo
);
543 tSumdInfoLen
= (size_t)usGetWord(0x11c, aucHeader
); /* cbSttbfAssoc */
544 DBG_DEC(tSumdInfoLen
);
546 if (tSumdInfoLen
== 0) {
547 /* There is no summary information */
551 aucBuffer
= xmalloc(tSumdInfoLen
);
552 if (!bReadBytes(aucBuffer
, tSumdInfoLen
, ulBeginSumdInfo
, pFile
)) {
553 aucBuffer
= xfree(aucBuffer
);
556 NO_DBG_PRINT_BLOCK(aucBuffer
, tSumdInfoLen
);
557 tLen
= (size_t)ucGetByte(0, aucBuffer
);
558 DBG_DEC_C(tSumdInfoLen
!= tLen
, tSumdInfoLen
);
559 DBG_DEC_C(tSumdInfoLen
!= tLen
, tLen
);
561 for (tCounter
= 0; tCounter
< 18; tCounter
++) {
562 if (tStart
>= tSumdInfoLen
) {
565 tLen
= (size_t)ucGetByte(tStart
, aucBuffer
);
567 NO_DBG_DEC(tCounter
);
568 NO_DBG_STRN(aucBuffer
+ tStart
+ 1, tLen
);
571 szTitle
= xmalloc(tLen
+ 1);
573 (char *)aucBuffer
+ tStart
+ 1, tLen
);
574 szTitle
[tLen
] = '\0';
577 szSubject
= xmalloc(tLen
+ 1);
579 (char *)aucBuffer
+ tStart
+ 1, tLen
);
580 szSubject
[tLen
] = '\0';
583 szAuthor
= xmalloc(tLen
+ 1);
585 (char *)aucBuffer
+ tStart
+ 1, tLen
);
586 szAuthor
[tLen
] = '\0';
594 aucBuffer
= xfree(aucBuffer
);
596 /* Third check the document properties */
597 ulBeginDocpInfo
= ulGetLong(0x112, aucHeader
); /* fcDop */
598 DBG_HEX(ulBeginDocpInfo
);
599 tDocpInfoLen
= (size_t)usGetWord(0x116, aucHeader
); /* cbDop */
600 DBG_DEC(tDocpInfoLen
);
601 if (tDocpInfoLen
< 12) {
605 aucBuffer
= xmalloc(tDocpInfoLen
);
606 if (!bReadBytes(aucBuffer
, tDocpInfoLen
, ulBeginDocpInfo
, pFile
)) {
607 aucBuffer
= xfree(aucBuffer
);
610 ulTmp
= ulGetLong(0x14, aucBuffer
); /* dttmCreated */
611 tCreateDtm
= tConvertDTTM(ulTmp
);
612 ulTmp
= ulGetLong(0x18, aucBuffer
); /* dttmRevised */
613 tLastSaveDtm
= tConvertDTTM(ulTmp
);
614 aucBuffer
= xfree(aucBuffer
);
615 } /* end of vSet2SummaryInfo */
618 * vSetSummaryInfoOLE - set summary information from a Word 6+ file
621 vSetSummaryInfoOLE(FILE *pFile
, const pps_info_type
*pPPS
,
622 const ULONG
*aulBBD
, size_t tBBDLen
,
623 const ULONG
*aulSBD
, size_t tSBDLen
)
627 fail(pFile
== NULL
|| pPPS
== NULL
);
628 fail(aulBBD
== NULL
|| aulSBD
== NULL
);
630 /* Summary Information */
631 pucBuffer
= pucAnalyseSummaryInfoHeader(pFile
,
632 pPPS
->tSummaryInfo
.ulSB
, pPPS
->tSummaryInfo
.ulSize
,
633 aulBBD
, tBBDLen
, aulSBD
, tSBDLen
);
634 if (pucBuffer
!= NULL
) {
635 vAnalyseSummaryInfo(pucBuffer
);
636 pucBuffer
= xfree(pucBuffer
);
639 /* Document Summary Information */
640 pucBuffer
= pucAnalyseSummaryInfoHeader(pFile
,
641 pPPS
->tDocSummaryInfo
.ulSB
, pPPS
->tDocSummaryInfo
.ulSize
,
642 aulBBD
, tBBDLen
, aulSBD
, tSBDLen
);
643 if (pucBuffer
!= NULL
) {
644 vAnalyseDocumentSummaryInfo(pucBuffer
);
645 pucBuffer
= xfree(pucBuffer
);
647 } /* end of vSetSummaryInfoOLE */
650 * vSet6SummaryInfo - set summary information from a Word 6/7 file
653 vSet6SummaryInfo(FILE *pFile
, const pps_info_type
*pPPS
,
654 const ULONG
*aulBBD
, size_t tBBDLen
,
655 const ULONG
*aulSBD
, size_t tSBDLen
,
656 const UCHAR
*aucHeader
)
658 /* Header Information */
659 usLid
= usGetWord(0x06, aucHeader
); /* Language IDentification */
662 /* Summery Information */
663 vSetSummaryInfoOLE(pFile
, pPPS
, aulBBD
, tBBDLen
, aulSBD
, tSBDLen
);
664 } /* end of vSet6SummaryInfo */
667 * vSet8SummaryInfo - set summary information a Word 8/9/10 file
670 vSet8SummaryInfo(FILE *pFile
, const pps_info_type
*pPPS
,
671 const ULONG
*aulBBD
, size_t tBBDLen
,
672 const ULONG
*aulSBD
, size_t tSBDLen
,
673 const UCHAR
*aucHeader
)
677 /* Header Information */
678 usTmp
= usGetWord(0x0a, aucHeader
);
679 if (usTmp
& BIT(14)) {
680 /* Language IDentification Far East */
681 usLid
= usGetWord(0x3c, aucHeader
);
683 /* Language IDentification */
684 usLid
= usGetWord(0x06, aucHeader
);
688 /* Summery Information */
689 vSetSummaryInfoOLE(pFile
, pPPS
, aulBBD
, tBBDLen
, aulSBD
, tSBDLen
);
690 } /* end of vSet8SummaryInfo */
693 * szGetTitle - get the title field
699 } /* end of szGetTitle */
702 * szGetSubject - get the subject field
708 } /* end of szGetSubject */
711 * szGetAuthor - get the author field
717 } /* end of szGetAuthor */
720 * szGetLastSaveDtm - get the last save date field
723 szGetLastSaveDtm(void)
725 static char szTime
[12];
728 if (tLastSaveDtm
== (time_t)-1) {
731 pTime
= localtime(&tLastSaveDtm
);
735 sprintf(szTime
, "%04d-%02d-%02d",
736 pTime
->tm_year
+ 1900, pTime
->tm_mon
+ 1, pTime
->tm_mday
);
738 } /* end of szGetLastSaveDtm */
741 * szGetCompany - get the company field
747 } /* end of szGetCompany */
750 * szGetLanguage - get de language field
755 if (usLid
== (USHORT
)-1) {
756 /* No Language IDentification */
760 /* This is a Locale, not a Language IDentification */
765 /* Exceptions to the general rule */
767 case 0x0404: return "zh_TW"; /* Traditional Chinese */
768 case 0x0804: return "zh_CN"; /* Simplified Chinese */
769 case 0x0c04: return "zh_HK"; /* Hong Kong Chinese */
770 case 0x1004: return "zh_SG"; /* Singapore Chinese */
771 case 0x0807: return "de_CH"; /* Swiss German */
772 case 0x0409: return "en_US"; /* American English */
773 case 0x0809: return "en_GB"; /* British English */
774 case 0x0c09: return "en_AU"; /* Australian English */
775 case 0x080a: return "es_MX"; /* Mexican Spanish */
776 case 0x080c: return "fr_BE"; /* Belgian French */
777 case 0x0c0c: return "fr_CA"; /* Canadian French */
778 case 0x100c: return "fr_CH"; /* Swiss French */
779 case 0x0810: return "it_CH"; /* Swiss Italian */
780 case 0x0813: return "nl_BE"; /* Belgian Dutch */
781 case 0x0416: return "pt_BR"; /* Brazilian Portuguese */
783 case 0x0c1a: return "sr"; /* Serbian */
784 case 0x081d: return "sv_FI"; /* Finland Swedish */
789 /* The general rule */
790 switch (usLid
& 0x00ff) {
791 case 0x01: return "ar"; /* Arabic */
792 case 0x02: return "bg"; /* Bulgarian */
793 case 0x03: return "ca"; /* Catalan */
794 case 0x04: return "zh"; /* Chinese */
795 case 0x05: return "cs"; /* Czech */
796 case 0x06: return "da"; /* Danish */
797 case 0x07: return "de"; /* German */
798 case 0x08: return "el"; /* Greek */
799 case 0x09: return "en"; /* English */
800 case 0x0a: return "es"; /* Spanish */
801 case 0x0b: return "fi"; /* Finnish */
802 case 0x0c: return "fr"; /* French */
803 case 0x0d: return "he"; /* Hebrew */
804 case 0x0e: return "hu"; /* Hungarian */
805 case 0x0f: return "is"; /* Icelandic */
806 case 0x10: return "it"; /* Italian */
807 case 0x11: return "ja"; /* Japanese */
808 case 0x12: return "ko"; /* Korean */
809 case 0x13: return "nl"; /* Dutch */
810 case 0x14: return "no"; /* Norwegian */
811 case 0x15: return "pl"; /* Polish */
812 case 0x16: return "pt"; /* Portuguese */
813 case 0x17: return "rm"; /* Rhaeto-Romance */
814 case 0x18: return "ro"; /* Romanian */
815 case 0x19: return "ru"; /* Russian */
816 case 0x1a: return "hr"; /* Croatian */
817 case 0x1b: return "sk"; /* Slovak */
818 case 0x1c: return "sq"; /* Albanian */
819 case 0x1d: return "sv"; /* Swedish */
820 case 0x1e: return "th"; /* Thai */
821 case 0x1f: return "tr"; /* Turkish */
822 case 0x20: return "ur"; /* Urdu */
823 case 0x21: return "id"; /* Indonesian */
824 case 0x22: return "uk"; /* Ukrainian */
825 case 0x23: return "be"; /* Belarusian */
826 case 0x24: return "sl"; /* Slovenian */
827 case 0x25: return "et"; /* Estonian */
828 case 0x26: return "lv"; /* Latvian */
829 case 0x27: return "lt"; /* Lithuanian */
830 case 0x29: return "fa"; /* Farsi */
831 case 0x2a: return "vi"; /* Viet Nam */
832 case 0x2b: return "hy"; /* Armenian */
833 case 0x2c: return "az"; /* Azeri */
834 case 0x2d: return "eu"; /* Basque */
835 case 0x2f: return "mk"; /* Macedonian */
836 case 0x36: return "af"; /* Afrikaans */
837 case 0x37: return "ka"; /* Georgian */
838 case 0x38: return "fo"; /* Faeroese */
839 case 0x39: return "hi"; /* Hindi */
840 case 0x3e: return "ms"; /* Malay */
841 case 0x3f: return "kk"; /* Kazakh */
847 } /* end of szGetLanguage */