/* $Id$ */ /*************************************************************************** * chm_lib.c - CHM archive manipulation routines * * ------------------- * * * * author: Jed Wing * * version: 0.3 * * notes: These routines are meant for the manipulation of microsoft * * .chm (compiled html help) files, but may likely be used * * for the manipulation of any ITSS archive, if ever ITSS * * archives are used for any other purpose. * * * * Note also that the section names are statically handled. * * To be entirely correct, the section names should be read * * from the section names meta-file, and then the various * * content sections and the "transforms" to apply to the data * * they contain should be inferred from the section name and * * the meta-files referenced using that name; however, all of * * the files I've been able to get my hands on appear to have * * only two sections: Uncompressed and MSCompressed. * * Additionally, the ITSS.DLL file included with Windows does * * not appear to handle any different transforms than the * * simple LZX-transform. Furthermore, the list of transforms * * to apply is broken, in that only half the required space * * is allocated for the list. (It appears as though the * * space is allocated for ASCII strings, but the strings are * * written as unicode. As a result, only the first half of * * the string appears.) So this is probably not too big of * * a deal, at least until CHM v4 (MS .lit files), which also * * incorporate encryption, of some description. * * * * switches: CHM_MT: compile library with thread-safety * * * * switches (Linux only): * * CHM_USE_PREAD: compile library to use pread instead of * * lseek/read * * CHM_USE_IO64: compile library to support full 64-bit I/O * * as is needed to properly deal with the * * 64-bit file offsets. * ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation; either version 2.1 of the * * License, or (at your option) any later version. * * * ***************************************************************************/ #include "chm_lib.h" #ifdef CHM_MT #define _REENTRANT #endif #include "lzx.h" #include #include #if __sun || __sgi #include #endif #ifdef WIN32 #include #include #else /* basic Linux system includes */ #define _XOPEN_SOURCE 500 #include #include #include #include #include #endif /* includes/defines for threading, if using them */ #ifdef CHM_MT #ifdef WIN32 #define CHM_ACQUIRE_LOCK(a) do { \ EnterCriticalSection(&(a)); \ } while(0) #define CHM_RELEASE_LOCK(a) do { \ EnterCriticalSection(&(a)); \ } while(0) #else #include #define CHM_ACQUIRE_LOCK(a) do { \ pthread_mutex_lock(&(a)); \ } while(0) #define CHM_RELEASE_LOCK(a) do { \ pthread_mutex_unlock(&(a)); \ } while(0) #endif #else #define CHM_ACQUIRE_LOCK(a) /* do nothing */ #define CHM_RELEASE_LOCK(a) /* do nothing */ #endif #ifdef WIN32 #define CHM_NULL_FD (INVALID_HANDLE_VALUE) #define CHM_USE_WIN32IO 1 #define CHM_CLOSE_FILE(fd) CloseHandle((fd)) #else #define CHM_NULL_FD (-1) #define CHM_CLOSE_FILE(fd) close((fd)) #endif /* * defines related to tuning */ #ifndef CHM_MAX_BLOCKS_CACHED #define CHM_MAX_BLOCKS_CACHED 5 #endif /* * architecture specific defines * * Note: as soon as C99 is more widespread, the below defines should * probably just use the C99 sized-int types. * * The following settings will probably work for many platforms. The sizes * don't have to be exactly correct, but the types must accommodate at least as * many bits as they specify. */ /* i386, 32-bit, Windows */ #ifdef WIN32 typedef unsigned char UChar; typedef __int16 Int16; typedef unsigned __int16 UInt16; typedef __int32 Int32; typedef unsigned __int32 UInt32; typedef __int64 Int64; typedef unsigned __int64 UInt64; /* I386, 32-bit, non-Windows */ /* Sparc */ /* MIPS */ #else typedef unsigned char UChar; typedef short Int16; typedef unsigned short UInt16; typedef long Int32; typedef unsigned long UInt32; typedef long long Int64; typedef unsigned long long UInt64; #endif /* GCC */ #ifdef __GNUC__ #define memcmp __builtin_memcmp #define memcpy __builtin_memcpy #define strlen __builtin_strlen #elif defined(WIN32) static int ffs(unsigned int val) { int bit=1, idx=1; while (bit != 0 && (val & bit) == 0) { bit <<= 1; ++idx; } if (bit == 0) return 0; else return idx; } #endif /* utilities for unmarshalling data */ static int _unmarshal_char_array(unsigned char **pData, unsigned long *pLenRemain, char *dest, int count) { if (count <= 0 || (unsigned int)count > *pLenRemain) return 0; memcpy(dest, (*pData), count); *pData += count; *pLenRemain -= count; return 1; } static int _unmarshal_uchar_array(unsigned char **pData, unsigned long *pLenRemain, unsigned char *dest, int count) { if (count <= 0 || (unsigned int)count > *pLenRemain) return 0; memcpy(dest, (*pData), count); *pData += count; *pLenRemain -= count; return 1; } static int _unmarshal_int16(unsigned char **pData, unsigned long *pLenRemain, Int16 *dest) { if (2 > *pLenRemain) return 0; *dest = (*pData)[0] | (*pData)[1]<<8; *pData += 2; *pLenRemain -= 2; return 1; } static int _unmarshal_uint16(unsigned char **pData, unsigned long *pLenRemain, UInt16 *dest) { if (2 > *pLenRemain) return 0; *dest = (*pData)[0] | (*pData)[1]<<8; *pData += 2; *pLenRemain -= 2; return 1; } static int _unmarshal_int32(unsigned char **pData, unsigned long *pLenRemain, Int32 *dest) { if (4 > *pLenRemain) return 0; *dest = (*pData)[0] | (*pData)[1]<<8 | (*pData)[2]<<16 | (*pData)[3]<<24; *pData += 4; *pLenRemain -= 4; return 1; } static int _unmarshal_uint32(unsigned char **pData, unsigned long *pLenRemain, UInt32 *dest) { if (4 > *pLenRemain) return 0; *dest = (*pData)[0] | (*pData)[1]<<8 | (*pData)[2]<<16 | (*pData)[3]<<24; *pData += 4; *pLenRemain -= 4; return 1; } static int _unmarshal_int64(unsigned char **pData, unsigned long *pLenRemain, Int64 *dest) { Int64 temp; int i; if (8 > *pLenRemain) return 0; temp=0; for(i=8; i>0; i--) { temp <<= 8; temp |= (*pData)[i-1]; } *dest = temp; *pData += 8; *pLenRemain -= 8; return 1; } static int _unmarshal_uint64(unsigned char **pData, unsigned long *pLenRemain, UInt64 *dest) { UInt64 temp; int i; if (8 > *pLenRemain) return 0; temp=0; for(i=8; i>0; i--) { temp <<= 8; temp |= (*pData)[i-1]; } *dest = temp; *pData += 8; *pLenRemain -= 8; return 1; } static int _unmarshal_uuid(unsigned char **pData, unsigned long *pDataLen, unsigned char *dest) { return _unmarshal_uchar_array(pData, pDataLen, dest, 16); } /* names of sections essential to decompression */ static const char _CHMU_RESET_TABLE[] = "::DataSpace/Storage/MSCompressed/Transform/" "{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/" "InstanceData/ResetTable"; static const char _CHMU_LZXC_CONTROLDATA[] = "::DataSpace/Storage/MSCompressed/ControlData"; static const char _CHMU_CONTENT[] = "::DataSpace/Storage/MSCompressed/Content"; static const char _CHMU_SPANINFO[] = "::DataSpace/Storage/MSCompressed/SpanInfo"; /* * structures local to this module */ /* structure of ITSF headers */ #define _CHM_ITSF_V2_LEN (0x58) #define _CHM_ITSF_V3_LEN (0x60) struct chmItsfHeader { char signature[4]; /* 0 (ITSF) */ Int32 version; /* 4 */ Int32 header_len; /* 8 */ Int32 unknown_000c; /* c */ UInt32 last_modified; /* 10 */ UInt32 lang_id; /* 14 */ UChar dir_uuid[16]; /* 18 */ UChar stream_uuid[16]; /* 28 */ UInt64 unknown_offset; /* 38 */ UInt64 unknown_len; /* 40 */ UInt64 dir_offset; /* 48 */ UInt64 dir_len; /* 50 */ UInt64 data_offset; /* 58 (Not present before V3) */ }; /* __attribute__ ((aligned (1))); */ static int _unmarshal_itsf_header(unsigned char **pData, unsigned long *pDataLen, struct chmItsfHeader *dest) { /* we only know how to deal with the 0x58 and 0x60 byte structures */ if (*pDataLen != _CHM_ITSF_V2_LEN && *pDataLen != _CHM_ITSF_V3_LEN) return 0; /* unmarshal common fields */ _unmarshal_char_array(pData, pDataLen, dest->signature, 4); _unmarshal_int32 (pData, pDataLen, &dest->version); _unmarshal_int32 (pData, pDataLen, &dest->header_len); _unmarshal_int32 (pData, pDataLen, &dest->unknown_000c); _unmarshal_uint32 (pData, pDataLen, &dest->last_modified); _unmarshal_uint32 (pData, pDataLen, &dest->lang_id); _unmarshal_uuid (pData, pDataLen, dest->dir_uuid); _unmarshal_uuid (pData, pDataLen, dest->stream_uuid); _unmarshal_uint64 (pData, pDataLen, &dest->unknown_offset); _unmarshal_uint64 (pData, pDataLen, &dest->unknown_len); _unmarshal_uint64 (pData, pDataLen, &dest->dir_offset); _unmarshal_uint64 (pData, pDataLen, &dest->dir_len); /* error check the data */ /* XXX: should also check UUIDs, probably, though with a version 3 file, * current MS tools do not seem to use them. */ if (memcmp(dest->signature, "ITSF", 4) != 0) return 0; if (dest->version == 2) { if (dest->header_len < _CHM_ITSF_V2_LEN) return 0; } else if (dest->version == 3) { if (dest->header_len < _CHM_ITSF_V3_LEN) return 0; } else return 0; /* now, if we have a V3 structure, unmarshal the rest. * otherwise, compute it */ if (dest->version == 3) { if (*pDataLen != 0) _unmarshal_uint64(pData, pDataLen, &dest->data_offset); else return 0; } else dest->data_offset = dest->dir_offset + dest->dir_len; return 1; } /* structure of ITSP headers */ #define _CHM_ITSP_V1_LEN (0x54) struct chmItspHeader { char signature[4]; /* 0 (ITSP) */ Int32 version; /* 4 */ Int32 header_len; /* 8 */ Int32 unknown_000c; /* c */ UInt32 block_len; /* 10 */ Int32 blockidx_intvl; /* 14 */ Int32 index_depth; /* 18 */ Int32 index_root; /* 1c */ Int32 index_head; /* 20 */ Int32 unknown_0024; /* 24 */ UInt32 num_blocks; /* 28 */ Int32 unknown_002c; /* 2c */ UInt32 lang_id; /* 30 */ UChar system_uuid[16]; /* 34 */ UChar unknown_0044[16]; /* 44 */ }; /* __attribute__ ((aligned (1))); */ static int _unmarshal_itsp_header(unsigned char **pData, unsigned long *pDataLen, struct chmItspHeader *dest) { /* we only know how to deal with a 0x54 byte structures */ if (*pDataLen != _CHM_ITSP_V1_LEN) return 0; /* unmarshal fields */ _unmarshal_char_array(pData, pDataLen, dest->signature, 4); _unmarshal_int32 (pData, pDataLen, &dest->version); _unmarshal_int32 (pData, pDataLen, &dest->header_len); _unmarshal_int32 (pData, pDataLen, &dest->unknown_000c); _unmarshal_uint32 (pData, pDataLen, &dest->block_len); _unmarshal_int32 (pData, pDataLen, &dest->blockidx_intvl); _unmarshal_int32 (pData, pDataLen, &dest->index_depth); _unmarshal_int32 (pData, pDataLen, &dest->index_root); _unmarshal_int32 (pData, pDataLen, &dest->index_head); _unmarshal_int32 (pData, pDataLen, &dest->unknown_0024); _unmarshal_uint32 (pData, pDataLen, &dest->num_blocks); _unmarshal_int32 (pData, pDataLen, &dest->unknown_002c); _unmarshal_uint32 (pData, pDataLen, &dest->lang_id); _unmarshal_uuid (pData, pDataLen, dest->system_uuid); _unmarshal_uchar_array(pData, pDataLen, dest->unknown_0044, 16); /* error check the data */ if (memcmp(dest->signature, "ITSP", 4) != 0) return 0; if (dest->version != 1) return 0; if (dest->header_len != _CHM_ITSP_V1_LEN) return 0; return 1; } /* structure of PMGL headers */ static const char _chm_pmgl_marker[4] = "PMGL"; #define _CHM_PMGL_LEN (0x14) struct chmPmglHeader { char signature[4]; /* 0 (PMGL) */ UInt32 free_space; /* 4 */ UInt32 unknown_0008; /* 8 */ Int32 block_prev; /* c */ Int32 block_next; /* 10 */ }; /* __attribute__ ((aligned (1))); */ static int _unmarshal_pmgl_header(unsigned char **pData, unsigned long *pDataLen, struct chmPmglHeader *dest) { /* we only know how to deal with a 0x14 byte structures */ if (*pDataLen != _CHM_PMGL_LEN) return 0; /* unmarshal fields */ _unmarshal_char_array(pData, pDataLen, dest->signature, 4); _unmarshal_uint32 (pData, pDataLen, &dest->free_space); _unmarshal_uint32 (pData, pDataLen, &dest->unknown_0008); _unmarshal_int32 (pData, pDataLen, &dest->block_prev); _unmarshal_int32 (pData, pDataLen, &dest->block_next); /* check structure */ if (memcmp(dest->signature, _chm_pmgl_marker, 4) != 0) return 0; return 1; } /* structure of PMGI headers */ static const char _chm_pmgi_marker[4] = "PMGI"; #define _CHM_PMGI_LEN (0x08) struct chmPmgiHeader { char signature[4]; /* 0 (PMGI) */ UInt32 free_space; /* 4 */ }; /* __attribute__ ((aligned (1))); */ static int _unmarshal_pmgi_header(unsigned char **pData, unsigned long *pDataLen, struct chmPmgiHeader *dest) { /* we only know how to deal with a 0x8 byte structures */ if (*pDataLen != _CHM_PMGI_LEN) return 0; /* unmarshal fields */ _unmarshal_char_array(pData, pDataLen, dest->signature, 4); _unmarshal_uint32 (pData, pDataLen, &dest->free_space); /* check structure */ if (memcmp(dest->signature, _chm_pmgi_marker, 4) != 0) return 0; return 1; } /* structure of LZXC reset table */ #define _CHM_LZXC_RESETTABLE_V1_LEN (0x28) struct chmLzxcResetTable { UInt32 version; UInt32 block_count; UInt32 unknown; UInt32 table_offset; UInt64 uncompressed_len; UInt64 compressed_len; UInt64 block_len; }; /* __attribute__ ((aligned (1))); */ static int _unmarshal_lzxc_reset_table(unsigned char **pData, unsigned long *pDataLen, struct chmLzxcResetTable *dest) { /* we only know how to deal with a 0x28 byte structures */ if (*pDataLen != _CHM_LZXC_RESETTABLE_V1_LEN) return 0; /* unmarshal fields */ _unmarshal_uint32 (pData, pDataLen, &dest->version); _unmarshal_uint32 (pData, pDataLen, &dest->block_count); _unmarshal_uint32 (pData, pDataLen, &dest->unknown); _unmarshal_uint32 (pData, pDataLen, &dest->table_offset); _unmarshal_uint64 (pData, pDataLen, &dest->uncompressed_len); _unmarshal_uint64 (pData, pDataLen, &dest->compressed_len); _unmarshal_uint64 (pData, pDataLen, &dest->block_len); /* check structure */ if (dest->version != 2) return 0; return 1; } /* structure of LZXC control data block */ #define _CHM_LZXC_MIN_LEN (0x18) #define _CHM_LZXC_V2_LEN (0x1c) struct chmLzxcControlData { UInt32 size; /* 0 */ char signature[4]; /* 4 (LZXC) */ UInt32 version; /* 8 */ UInt32 resetInterval; /* c */ UInt32 windowSize; /* 10 */ UInt32 unknown_14; /* 14 */ UInt32 unknown_18; /* 18 */ }; static int _unmarshal_lzxc_control_data(unsigned char **pData, unsigned long *pDataLen, struct chmLzxcControlData *dest) { /* we want at least 0x18 bytes */ if (*pDataLen < _CHM_LZXC_MIN_LEN) return 0; /* unmarshal fields */ _unmarshal_uint32 (pData, pDataLen, &dest->size); _unmarshal_char_array(pData, pDataLen, dest->signature, 4); _unmarshal_uint32 (pData, pDataLen, &dest->version); _unmarshal_uint32 (pData, pDataLen, &dest->resetInterval); _unmarshal_uint32 (pData, pDataLen, &dest->windowSize); _unmarshal_uint32 (pData, pDataLen, &dest->unknown_14); if (*pDataLen >= _CHM_LZXC_V2_LEN) _unmarshal_uint32 (pData, pDataLen, &dest->unknown_18); else dest->unknown_18 = 0; if (dest->version == 2) { dest->resetInterval *= 0x8000; dest->windowSize *= 0x8000; dest->unknown_14 *= 0x8000; } if (dest->windowSize == 0 || dest->resetInterval == 0) return 0; /* for now, only support resetInterval a multiple of windowSize/2 */ if (dest->windowSize == 1) return 0; if ((dest->resetInterval % (dest->windowSize/2)) != 0) return 0; /* check structure */ if (memcmp(dest->signature, "LZXC", 4) != 0) return 0; return 1; } /* the structure used for chm file handles */ struct chmFile { #ifdef WIN32 HANDLE fd; #else int fd; #endif #ifdef CHM_MT #ifdef WIN32 CRITICAL_SECTION mutex; CRITICAL_SECTION lzx_mutex; CRITICAL_SECTION cache_mutex; #else pthread_mutex_t mutex; pthread_mutex_t lzx_mutex; pthread_mutex_t cache_mutex; #endif #endif UInt64 dir_offset; UInt64 dir_len; UInt64 data_offset; Int32 index_root; Int32 index_head; UInt32 block_len; UInt64 span; struct chmUnitInfo rt_unit; struct chmUnitInfo cn_unit; struct chmLzxcResetTable reset_table; /* LZX control data */ UInt32 window_size; UInt32 reset_interval; UInt32 reset_blkcount; /* decompressor state */ struct LZXstate *lzx_state; int lzx_last_block; /* cache for decompressed blocks */ UChar **cache_blocks; Int64 *cache_block_indices; Int32 cache_num_blocks; }; /* * utility functions local to this module */ /* utility function to handle differences between {pread,read}(64)? */ static Int64 _chm_fetch_bytes(struct chmFile *h, UChar *buf, UInt64 os, Int64 len) { Int64 readLen=0, oldOs=0; if (h->fd == CHM_NULL_FD) return readLen; CHM_ACQUIRE_LOCK(h->mutex); #ifdef CHM_USE_WIN32IO /* NOTE: this might be better done with CreateFileMapping, et cetera... */ { DWORD origOffsetLo=0, origOffsetHi=0; DWORD offsetLo, offsetHi; DWORD actualLen=0; /* awkward Win32 Seek/Tell */ offsetLo = (unsigned long)(os & 0xffffffffL); offsetHi = (unsigned long)((os >> 32) & 0xffffffffL); origOffsetLo = SetFilePointer(h->fd, 0, &origOffsetHi, FILE_CURRENT); offsetLo = SetFilePointer(h->fd, offsetLo, &offsetHi, FILE_BEGIN); /* read the data */ if (ReadFile(h->fd, buf, (DWORD)len, &actualLen, NULL) == TRUE) readLen = actualLen; else readLen = 0; /* restore original position */ SetFilePointer(h->fd, origOffsetLo, &origOffsetHi, FILE_BEGIN); } #else #ifdef CHM_USE_PREAD #ifdef CHM_USE_IO64 readLen = pread64(h->fd, buf, (long)len, os); #else readLen = pread(h->fd, buf, (long)len, (unsigned long)os); #endif #else #ifdef CHM_USE_IO64 oldOs = lseek64(h->fd, 0, SEEK_CUR); lseek64(h->fd, os, SEEK_SET); readLen = read(h->fd, buf, len); lseek64(h->fd, oldOs, SEEK_SET); #else oldOs = lseek(h->fd, 0, SEEK_CUR); lseek(h->fd, (long)os, SEEK_SET); readLen = read(h->fd, buf, len); lseek(h->fd, (long)oldOs, SEEK_SET); #endif #endif #endif CHM_RELEASE_LOCK(h->mutex); return readLen; } /* open an ITS archive */ struct chmFile *chm_open(const char *filename) { unsigned char sbuffer[256]; unsigned long sremain; unsigned char *sbufpos; struct chmFile *newHandle=NULL; struct chmItsfHeader itsfHeader; struct chmItspHeader itspHeader; struct chmUnitInfo uiSpan; struct chmUnitInfo uiLzxc; struct chmLzxcControlData ctlData; /* allocate handle */ newHandle = (struct chmFile *)malloc(sizeof(struct chmFile)); newHandle->fd = CHM_NULL_FD; newHandle->lzx_state = NULL; newHandle->cache_blocks = NULL; newHandle->cache_block_indices = NULL; newHandle->cache_num_blocks = 0; /* open file */ #ifdef WIN32 if ((newHandle->fd=CreateFileA(filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == CHM_NULL_FD) { free(newHandle); return NULL; } #else if ((newHandle->fd=open(filename, O_RDONLY)) == CHM_NULL_FD) { free(newHandle); return NULL; } #endif /* initialize mutexes, if needed */ #ifdef CHM_MT #ifdef WIN32 InitializeCriticalSection(&newHandle->mutex); InitializeCriticalSection(&newHandle->lzx_mutex); InitializeCriticalSection(&newHandle->cache_mutex); #else pthread_mutex_init(&newHandle->mutex, NULL); pthread_mutex_init(&newHandle->lzx_mutex, NULL); pthread_mutex_init(&newHandle->cache_mutex, NULL); #endif #endif /* read and verify header */ sremain = _CHM_ITSF_V3_LEN; sbufpos = sbuffer; if (_chm_fetch_bytes(newHandle, sbuffer, (UInt64)0, sremain) != sremain || !_unmarshal_itsf_header(&sbufpos, &sremain, &itsfHeader)) { chm_close(newHandle); return NULL; } /* stash important values from header */ newHandle->dir_offset = itsfHeader.dir_offset; newHandle->dir_len = itsfHeader.dir_len; newHandle->data_offset = itsfHeader.data_offset; /* now, read and verify the directory header chunk */ sremain = _CHM_ITSP_V1_LEN; sbufpos = sbuffer; if (_chm_fetch_bytes(newHandle, sbuffer, (UInt64)itsfHeader.dir_offset, sremain) != sremain || !_unmarshal_itsp_header(&sbufpos, &sremain, &itspHeader)) { chm_close(newHandle); return NULL; } /* grab essential information from ITSP header */ newHandle->dir_offset += itspHeader.header_len; newHandle->dir_len -= itspHeader.header_len; newHandle->index_root = itspHeader.index_root; newHandle->index_head = itspHeader.index_head; newHandle->block_len = itspHeader.block_len; /* if the index root is -1, this means we don't have any PMGI blocks. * as a result, we must use the sole PMGL block as the index root */ if (newHandle->index_root == -1) newHandle->index_root = newHandle->index_head; /* prefetch most commonly needed unit infos */ if (CHM_RESOLVE_SUCCESS != chm_resolve_object(newHandle, _CHMU_SPANINFO, &uiSpan) || uiSpan.space == CHM_COMPRESSED || CHM_RESOLVE_SUCCESS != chm_resolve_object(newHandle, _CHMU_RESET_TABLE, &newHandle->rt_unit) || newHandle->rt_unit.space == CHM_COMPRESSED || CHM_RESOLVE_SUCCESS != chm_resolve_object(newHandle, _CHMU_CONTENT, &newHandle->cn_unit) || newHandle->cn_unit.space == CHM_COMPRESSED || CHM_RESOLVE_SUCCESS != chm_resolve_object(newHandle, _CHMU_LZXC_CONTROLDATA, &uiLzxc) || uiLzxc.space == CHM_COMPRESSED) { chm_close(newHandle); return NULL; } /* try to read span */ /* N.B.: we've already checked that uiSpan is in the uncompressed section, * so this should not require attempting to decompress, which may * rely on having a valid "span" */ sremain = 8; sbufpos = sbuffer; if (chm_retrieve_object(newHandle, &uiSpan, sbuffer, 0, sremain) != sremain || !_unmarshal_uint64(&sbufpos, &sremain, &newHandle->span)) { chm_close(newHandle); return NULL; } /* read reset table info */ sremain = _CHM_LZXC_RESETTABLE_V1_LEN; sbufpos = sbuffer; if (chm_retrieve_object(newHandle, &newHandle->rt_unit, sbuffer, 0, sremain) != sremain || !_unmarshal_lzxc_reset_table(&sbufpos, &sremain, &newHandle->reset_table)) { chm_close(newHandle); return NULL; } /* read control data */ sremain = (unsigned long)uiLzxc.length; sbufpos = sbuffer; if (chm_retrieve_object(newHandle, &uiLzxc, sbuffer, 0, sremain) != sremain || !_unmarshal_lzxc_control_data(&sbufpos, &sremain, &ctlData)) { chm_close(newHandle); return NULL; } newHandle->window_size = ctlData.windowSize; newHandle->reset_interval = ctlData.resetInterval; newHandle->reset_blkcount = newHandle->reset_interval / (newHandle->window_size / 2); /* initialize cache */ chm_set_param(newHandle, CHM_PARAM_MAX_BLOCKS_CACHED, CHM_MAX_BLOCKS_CACHED); return newHandle; } /* close an ITS archive */ void chm_close(struct chmFile *h) { if (h != NULL) { if (h->fd != CHM_NULL_FD) CHM_CLOSE_FILE(h->fd); h->fd = CHM_NULL_FD; #ifdef CHM_MT #ifdef WIN32 DeleteCriticalSection(&h->mutex); DeleteCriticalSection(&h->lzx_mutex); DeleteCriticalSection(&h->cache_mutex); #else pthread_mutex_destroy(&h->mutex); pthread_mutex_destroy(&h->lzx_mutex); pthread_mutex_destroy(&h->cache_mutex); #endif #endif if (h->lzx_state) LZXteardown(h->lzx_state); h->lzx_state = NULL; if (h->cache_blocks) { int i; for (i=0; icache_num_blocks; i++) { if (h->cache_blocks[i]) free(h->cache_blocks[i]); } free(h->cache_blocks); h->cache_blocks = NULL; } if (h->cache_block_indices) free(h->cache_block_indices); h->cache_block_indices = NULL; free(h); } } /* * set a parameter on the file handle. * valid parameter types: * CHM_PARAM_MAX_BLOCKS_CACHED: * how many decompressed blocks should be cached? A simple * caching scheme is used, wherein the index of the block is * used as a hash value, and hash collision results in the * invalidation of the previously cached block. */ void chm_set_param(struct chmFile *h, int paramType, int paramVal) { switch (paramType) { case CHM_PARAM_MAX_BLOCKS_CACHED: CHM_ACQUIRE_LOCK(h->cache_mutex); if (paramVal != h->cache_num_blocks) { UChar **newBlocks; UInt64 *newIndices; int i; /* allocate new cached blocks */ newBlocks = (UChar **)malloc(paramVal * sizeof (UChar *)); newIndices = (UInt64 *)malloc(paramVal * sizeof (UInt64)); for (i=0; icache_blocks) { for (i=0; icache_num_blocks; i++) { int newSlot = (int)(h->cache_block_indices[i] % paramVal); if (h->cache_blocks[i]) { /* in case of collision, destroy newcomer */ if (newBlocks[newSlot]) { free(h->cache_blocks[i]); h->cache_blocks[i] = NULL; } else { newBlocks[newSlot] = h->cache_blocks[i]; newIndices[newSlot] = h->cache_block_indices[i]; } } } free(h->cache_blocks); free(h->cache_block_indices); } /* now, set new values */ h->cache_blocks = newBlocks; h->cache_block_indices = newIndices; h->cache_num_blocks = paramVal; } CHM_RELEASE_LOCK(h->cache_mutex); break; default: break; } } /* * helper methods for chm_resolve_object */ /* skip a compressed dword */ static void _chm_skip_cword(UChar **pEntry) { while (*(*pEntry)++ >= 0x80) ; } /* skip the data from a PMGL entry */ static void _chm_skip_PMGL_entry_data(UChar **pEntry) { _chm_skip_cword(pEntry); _chm_skip_cword(pEntry); _chm_skip_cword(pEntry); } /* parse a compressed dword */ static UInt64 _chm_parse_cword(UChar **pEntry) { UInt64 accum = 0; UChar temp; while ((temp=*(*pEntry)++) >= 0x80) { accum <<= 7; accum += temp & 0x7f; } return (accum << 7) + temp; } /* parse a utf-8 string into an ASCII char buffer */ static int _chm_parse_UTF8(UChar **pEntry, UInt64 count, char *path) { /* XXX: implement UTF-8 support, including a real mapping onto * ISO-8859-1? probably there is a library to do this? As is * immediately apparent from the below code, I'm only handling files * in which none of the strings contain UTF-8 multi-byte characters. */ while (count != 0) { if (*(*pEntry) > 0x7f) return 0; *path++ = (char)(*(*pEntry)++); --count; } *path = '\0'; return 1; } /* parse a PMGL entry into a chmUnitInfo struct; return 1 on success. */ static int _chm_parse_PMGL_entry(UChar **pEntry, struct chmUnitInfo *ui) { UInt64 strLen; /* parse str len */ strLen = _chm_parse_cword(pEntry); if (strLen > CHM_MAX_PATHLEN) return 0; /* parse path */ if (! _chm_parse_UTF8(pEntry, strLen, ui->path)) return 0; /* parse info */ ui->space = (int)_chm_parse_cword(pEntry); ui->start = _chm_parse_cword(pEntry); ui->length = _chm_parse_cword(pEntry); return 1; } /* find an exact entry in PMGL; return NULL if we fail */ static UChar *_chm_find_in_PMGL(UChar *page_buf, UInt32 block_len, const char *objPath) { /* XXX: modify this to do a binary search using the nice index structure * that is provided for us. */ struct chmPmglHeader header; UInt32 hremain; UChar *end; UChar *cur; UChar *temp; UInt64 strLen; char buffer[CHM_MAX_PATHLEN+1]; /* figure out where to start and end */ cur = page_buf; hremain = _CHM_PMGL_LEN; if (! _unmarshal_pmgl_header(&cur, &hremain, &header)) return NULL; end = page_buf + block_len - (header.free_space); /* now, scan progressively */ while (cur < end) { /* grab the name */ temp = cur; strLen = _chm_parse_cword(&cur); if (! _chm_parse_UTF8(&cur, strLen, buffer)) return NULL; /* check if it is the right name */ #ifdef WIN32 if (! stricmp(buffer, objPath)) return temp; #else if (! strcasecmp(buffer, objPath)) return temp; #endif _chm_skip_PMGL_entry_data(&cur); } return NULL; } /* find which block should be searched next for the entry; -1 if no block */ static Int32 _chm_find_in_PMGI(UChar *page_buf, UInt32 block_len, const char *objPath) { /* XXX: modify this to do a binary search using the nice index structure * that is provided for us */ struct chmPmgiHeader header; UInt32 hremain; int page=-1; UChar *end; UChar *cur; UInt64 strLen; char buffer[CHM_MAX_PATHLEN+1]; /* figure out where to start and end */ cur = page_buf; hremain = _CHM_PMGI_LEN; if (! _unmarshal_pmgi_header(&cur, &hremain, &header)) return -1; end = page_buf + block_len - (header.free_space); /* now, scan progressively */ while (cur < end) { /* grab the name */ strLen = _chm_parse_cword(&cur); if (! _chm_parse_UTF8(&cur, strLen, buffer)) return -1; /* check if it is the right name */ #ifdef WIN32 if (stricmp(buffer, objPath) > 0) return page; #else if (strcasecmp(buffer, objPath) > 0) return page; #endif /* load next value for path */ page = (int)_chm_parse_cword(&cur); } return page; } /* resolve a particular object from the archive */ int chm_resolve_object(struct chmFile *h, const char *objPath, struct chmUnitInfo *ui) { /* * XXX: implement caching scheme for dir pages */ Int32 curPage; /* buffer to hold whatever page we're looking at */ #ifdef WIN32 UChar *page_buf = alloca(h->block_len); #else UChar page_buf[h->block_len]; #endif /* starting page */ curPage = h->index_root; /* until we have either returned or given up */ while (curPage != -1) { /* try to fetch the index page */ if (_chm_fetch_bytes(h, page_buf, (UInt64)h->dir_offset + (UInt64)curPage*h->block_len, h->block_len) != h->block_len) return CHM_RESOLVE_FAILURE; /* now, if it is a leaf node: */ if (memcmp(page_buf, _chm_pmgl_marker, 4) == 0) { /* scan block */ UChar *pEntry = _chm_find_in_PMGL(page_buf, h->block_len, objPath); if (pEntry == NULL) return CHM_RESOLVE_FAILURE; /* parse entry and return */ _chm_parse_PMGL_entry(&pEntry, ui); return CHM_RESOLVE_SUCCESS; } /* else, if it is a branch node: */ else if (memcmp(page_buf, _chm_pmgi_marker, 4) == 0) curPage = _chm_find_in_PMGI(page_buf, h->block_len, objPath); /* else, we are confused. give up. */ else return CHM_RESOLVE_FAILURE; } /* didn't find anything. fail. */ return CHM_RESOLVE_FAILURE; } /* * utility methods for dealing with compressed data */ /* get the bounds of a compressed block. return 0 on failure */ static int _chm_get_cmpblock_bounds(struct chmFile *h, UInt64 block, UInt64 *start, Int64 *len) { UChar buffer[8], *dummy; UInt32 remain; /* for all but the last block, use the reset table */ if (block < h->reset_table.block_count-1) { /* unpack the start address */ dummy = buffer; remain = 8; if (_chm_fetch_bytes(h, buffer, (UInt64)h->data_offset + (UInt64)h->rt_unit.start + (UInt64)h->reset_table.table_offset + (UInt64)block*8, remain) != remain || !_unmarshal_uint64(&dummy, &remain, start)) return 0; /* unpack the end address */ dummy = buffer; remain = 8; if (_chm_fetch_bytes(h, buffer, (UInt64)h->data_offset + (UInt64)h->rt_unit.start + (UInt64)h->reset_table.table_offset + (UInt64)block*8 + 8, remain) != remain || !_unmarshal_int64(&dummy, &remain, len)) return 0; } /* for the last block, use the span in addition to the reset table */ else { /* unpack the start address */ dummy = buffer; remain = 8; if (_chm_fetch_bytes(h, buffer, (UInt64)h->data_offset + (UInt64)h->rt_unit.start + (UInt64)h->reset_table.table_offset + (UInt64)block*8, remain) != remain || !_unmarshal_uint64(&dummy, &remain, start)) return 0; *len = h->reset_table.compressed_len; } /* compute the length and absolute start address */ *len -= *start; *start += h->data_offset + h->cn_unit.start; return 1; } /* decompress the block. must have lzx_mutex. */ static Int64 _chm_decompress_block(struct chmFile *h, UInt64 block, UChar **ubuffer) { #ifdef WIN32 UChar *cbuffer = alloca(((unsigned int)h->reset_table.block_len + 6144)); #else UChar cbuffer[h->reset_table.block_len + 6144]; /* compressed buffer */ #endif UInt64 cmpStart; /* compressed start */ Int64 cmpLen; /* compressed len */ int indexSlot; /* cache index slot */ UChar *lbuffer; /* local buffer ptr */ UInt32 blockAlign = (UInt32)(block % h->reset_blkcount); /* reset intvl. aln. */ UInt32 i; /* local loop index */ /* check if we need previous blocks */ if (blockAlign != 0) { /* fetch all required previous blocks since last reset */ for (i = h->reset_blkcount - blockAlign; i > 0; i--) { /* check if we most recently decompressed the previous block */ if (h->lzx_last_block != block-i) { indexSlot = (int)((block-i) % h->cache_num_blocks); h->cache_block_indices[indexSlot] = block-i; if (! h->cache_blocks[indexSlot]) h->cache_blocks[indexSlot] = (UChar *)malloc( (unsigned int)(h->reset_table.block_len)); lbuffer = h->cache_blocks[indexSlot]; /* decompress the previous block */ LZXreset(h->lzx_state); if (!_chm_get_cmpblock_bounds(h, block-i, &cmpStart, &cmpLen) || _chm_fetch_bytes(h, cbuffer, cmpStart, cmpLen) != cmpLen || LZXdecompress(h->lzx_state, cbuffer, lbuffer, (int)cmpLen, (int)h->reset_table.block_len) != DECR_OK) return (Int64)0; } h->lzx_last_block = (int)(block - i); } } else LZXreset(h->lzx_state); /* allocate slot in cache */ indexSlot = (int)(block % h->cache_num_blocks); h->cache_block_indices[indexSlot] = block; if (! h->cache_blocks[indexSlot]) h->cache_blocks[indexSlot] = (UChar *)malloc( ((unsigned int)h->reset_table.block_len)); lbuffer = h->cache_blocks[indexSlot]; *ubuffer = lbuffer; /* decompress the block we actually want */ if (! _chm_get_cmpblock_bounds(h, block, &cmpStart, &cmpLen) || _chm_fetch_bytes(h, cbuffer, cmpStart, cmpLen) != cmpLen || LZXdecompress(h->lzx_state, cbuffer, lbuffer, (int)cmpLen, (int)h->reset_table.block_len) != DECR_OK) return (Int64)0; h->lzx_last_block = (int)block; /* XXX: modify LZX routines to return the length of the data they * decompressed and return that instead, for an extra sanity check. */ return h->reset_table.block_len; } /* grab a region from a compressed block */ static Int64 _chm_decompress_region(struct chmFile *h, UChar *buf, UInt64 start, Int64 len) { UInt64 nBlock, nOffset; UInt64 nLen; UInt64 gotLen; UChar *ubuffer; if (len <= 0) return (Int64)0; /* figure out what we need to read */ nBlock = start / h->reset_table.block_len; nOffset = start % h->reset_table.block_len; nLen = len; if (nLen > (h->reset_table.block_len - nOffset)) nLen = h->reset_table.block_len - nOffset; /* if block is cached, return data from it. */ CHM_ACQUIRE_LOCK(h->lzx_mutex); CHM_ACQUIRE_LOCK(h->cache_mutex); if (h->cache_block_indices[nBlock % h->cache_num_blocks] == nBlock && h->cache_blocks[nBlock % h->cache_num_blocks] != NULL) { memcpy(buf, h->cache_blocks[nBlock % h->cache_num_blocks] + nOffset, (unsigned int)nLen); CHM_RELEASE_LOCK(h->cache_mutex); CHM_RELEASE_LOCK(h->lzx_mutex); return nLen; } CHM_RELEASE_LOCK(h->cache_mutex); /* data request not satisfied, so... start up the decompressor machine */ if (! h->lzx_state) { int window_size = ffs(h->window_size) - 1; h->lzx_last_block = -1; h->lzx_state = LZXinit(window_size); } /* decompress some data */ gotLen = _chm_decompress_block(h, nBlock, &ubuffer); if (gotLen < nLen) nLen = gotLen; memcpy(buf, ubuffer+nOffset, (unsigned int)nLen); CHM_RELEASE_LOCK(h->lzx_mutex); return nLen; } /* retrieve (part of) an object */ LONGINT64 chm_retrieve_object(struct chmFile *h, struct chmUnitInfo *ui, unsigned char *buf, LONGUINT64 addr, LONGINT64 len) { /* must be valid file handle */ if (h == NULL) return (Int64)0; /* starting address must be in correct range */ if (addr < 0 || addr >= ui->length) return (Int64)0; /* clip length */ if (addr + len > ui->length) len = ui->length - addr; /* if the file is uncompressed, it's simple */ if (ui->space == CHM_UNCOMPRESSED) { /* read data */ return _chm_fetch_bytes(h, buf, (UInt64)h->data_offset + (UInt64)ui->start + (UInt64)addr, len); } /* else if the file is compressed, it's a little trickier */ else /* ui->space == CHM_COMPRESSED */ { Int64 swath=0, total=0; do { /* swill another mouthful */ swath = _chm_decompress_region(h, buf, ui->start + addr, len); /* if we didn't get any... */ if (swath == 0) return total; /* update stats */ total += swath; len -= swath; addr += swath; buf += swath; } while (len != 0); return total; } } /* enumerate the objects in the .chm archive */ int chm_enumerate(struct chmFile *h, int what, CHM_ENUMERATOR e, void *context) { Int32 curPage; /* buffer to hold whatever page we're looking at */ #ifdef WIN32 UChar *page_buf = alloca((unsigned int)h->block_len); #else UChar page_buf[h->block_len]; #endif struct chmPmglHeader header; UChar *end; UChar *cur; unsigned long lenRemain; /* the current ui */ struct chmUnitInfo ui; int flag; /* starting page */ curPage = h->index_head; /* until we have either returned or given up */ while (curPage != -1) { /* try to fetch the index page */ if (_chm_fetch_bytes(h, page_buf, (UInt64)h->dir_offset + (UInt64)curPage*h->block_len, h->block_len) != h->block_len) return 0; /* figure out start and end for this page */ cur = page_buf; lenRemain = _CHM_PMGL_LEN; if (! _unmarshal_pmgl_header(&cur, &lenRemain, &header)) return 0; end = page_buf + h->block_len - (header.free_space); /* loop over this page */ while (cur < end) { if (! _chm_parse_PMGL_entry(&cur, &ui)) return 0; /* check for DIRS */ if (ui.length == 0 && !(what & CHM_ENUMERATE_DIRS)) continue; /* check for FILES */ if (ui.length != 0 && !(what & CHM_ENUMERATE_FILES)) continue; /* check for NORMAL vs. META */ if (ui.path[0] == '/') { /* check for NORMAL vs. SPECIAL */ if (ui.path[1] == '#' || ui.path[1] == '$') flag = CHM_ENUMERATE_SPECIAL; else flag = CHM_ENUMERATE_NORMAL; } else flag = CHM_ENUMERATE_META; if (! (what & flag)) continue; /* call the enumerator */ { int status = (*e)(h, &ui, context); switch (status) { case CHM_ENUMERATOR_FAILURE: return 0; case CHM_ENUMERATOR_CONTINUE: break; case CHM_ENUMERATOR_SUCCESS: return 1; default: break; } } } /* advance to next page */ curPage = header.block_next; } return 1; } int chm_enumerate_dir(struct chmFile *h, const char *prefix, int what, CHM_ENUMERATOR e, void *context) { /* * XXX: do this efficiently (i.e. using the tree index) */ Int32 curPage; /* buffer to hold whatever page we're looking at */ #ifdef WIN32 UChar *page_buf = alloca((unsigned int)h->block_len); #else UChar page_buf[h->block_len]; #endif struct chmPmglHeader header; UChar *end; UChar *cur; unsigned long lenRemain; /* set to 1 once we've started */ int it_has_begun=0; /* the current ui */ struct chmUnitInfo ui; int flag; /* the length of the prefix */ char prefixRectified[CHM_MAX_PATHLEN+1]; int prefixLen; char lastPath[CHM_MAX_PATHLEN+1]; int lastPathLen; /* starting page */ curPage = h->index_head; /* initialize pathname state */ strncpy(prefixRectified, prefix, CHM_MAX_PATHLEN); prefixLen = strlen(prefixRectified); if (prefixLen != 0) { if (prefixRectified[prefixLen-1] != '/') { prefixRectified[prefixLen] = '/'; prefixRectified[prefixLen+1] = '\0'; ++prefixLen; } } lastPath[0] = '\0'; lastPathLen = -1; /* until we have either returned or given up */ while (curPage != -1) { /* try to fetch the index page */ if (_chm_fetch_bytes(h, page_buf, (UInt64)h->dir_offset + (UInt64)curPage*h->block_len, h->block_len) != h->block_len) return 0; /* figure out start and end for this page */ cur = page_buf; lenRemain = _CHM_PMGL_LEN; if (! _unmarshal_pmgl_header(&cur, &lenRemain, &header)) return 0; end = page_buf + h->block_len - (header.free_space); /* loop over this page */ while (cur < end) { if (! _chm_parse_PMGL_entry(&cur, &ui)) return 0; /* check if we should start */ if (! it_has_begun) { if (ui.length == 0 && strncmp(ui.path, prefixRectified, prefixLen) == 0) it_has_begun = 1; else continue; if (ui.path[prefixLen] == '\0') continue; } /* check if we should stop */ else { if (strncmp(ui.path, prefixRectified, prefixLen) != 0) return 1; } /* check if we should include this path */ if (lastPathLen != -1) { if (strncmp(ui.path, lastPath, lastPathLen) == 0) continue; } strncpy(lastPath, ui.path, CHM_MAX_PATHLEN); lastPath[CHM_MAX_PATHLEN] = '\0'; lastPathLen = strlen(lastPath); /* check for DIRS */ if (ui.length == 0 && !(what & CHM_ENUMERATE_DIRS)) continue; /* check for FILES */ if (ui.length != 0 && !(what & CHM_ENUMERATE_FILES)) continue; /* check for NORMAL vs. META */ if (ui.path[0] == '/') { /* check for NORMAL vs. SPECIAL */ if (ui.path[1] == '#' || ui.path[1] == '$') flag = CHM_ENUMERATE_SPECIAL; else flag = CHM_ENUMERATE_NORMAL; } else flag = CHM_ENUMERATE_META; if (! (what & flag)) continue; /* call the enumerator */ { int status = (*e)(h, &ui, context); switch (status) { case CHM_ENUMERATOR_FAILURE: return 0; case CHM_ENUMERATOR_CONTINUE: break; case CHM_ENUMERATOR_SUCCESS: return 1; default: break; } } } /* advance to next page */ curPage = header.block_next; } return 1; } /* resolve a particular object from the archive */ int chm_resolve_location(struct chmFile *h, unsigned long pos, struct chmUnitInfo *ui) { /* * XXX: implement caching scheme for dir pages */ Int32 curPage; /* buffer to hold whatever page we're looking at */ #ifdef WIN32 UChar *page_buf = alloca(h->block_len); #else UChar page_buf[h->block_len]; #endif /* starting page */ curPage = h->index_root; /* until we have either returned or given up */ while (curPage != -1) { /* try to fetch the index page */ if (_chm_fetch_bytes(h, page_buf, (UInt64)h->dir_offset + (UInt64)curPage*h->block_len, h->block_len) != h->block_len) return CHM_RESOLVE_FAILURE; /* now, if it is a leaf node: */ if (memcmp(page_buf, _chm_pmgl_marker, 4) == 0) { /* scan block */ /* UChar *pEntry = _chm_find_in_PMGL(page_buf, h->block_len, objPath);*/ { /* XXX: modify this to do a binary search using the nice index structure * that is provided for us. */ struct chmPmglHeader header; UInt32 hremain; UChar *end; UChar *cur; UChar *temp; /* UInt64 strLen; char buffer[CHM_MAX_PATHLEN+1]; */ /* figure out where to start and end */ cur = page_buf; hremain = _CHM_PMGL_LEN; if (! _unmarshal_pmgl_header(&cur, &hremain, &header)) return CHM_RESOLVE_FAILURE; end = page_buf + h->block_len - (header.free_space); /* now, scan progressively */ while (cur < end) { UInt32 st = 0; UInt32 nd = 0; /* grab the name */ temp = cur; if (_chm_parse_PMGL_entry(&cur, ui) == 0) { return CHM_RESOLVE_FAILURE; } st = ui->start; nd = ui->start+ui->length; if ((st <= pos) && (pos < nd)) { printf("Resolve:[%u,%u,%u]\n", st, pos, nd); return CHM_RESOLVE_SUCCESS; } } return CHM_RESOLVE_FAILURE; } } /* else, if it is a branch node: */ else if (memcmp(page_buf, _chm_pmgi_marker, 4) == 0) { /* curPage = _chm_find_in_PMGI(page_buf, h->block_len, objPath);*/ return CHM_RESOLVE_FAILURE; if (0) { /* XXX: modify this to do a binary search using the nice index structure * that is provided for us */ struct chmPmgiHeader header; UInt32 hremain; int page=-1; UChar *end; UChar *cur; UInt64 strLen; char buffer[CHM_MAX_PATHLEN+1]; /* figure out where to start and end */ cur = page_buf; hremain = _CHM_PMGI_LEN; if (! _unmarshal_pmgi_header(&cur, &hremain, &header)) return -1; end = page_buf + h->block_len - (header.free_space); /* now, scan progressively */ while (cur < end) { if (_chm_parse_PMGL_entry(&cur, ui) == 0) { return CHM_RESOLVE_FAILURE; } if (ui->start <= pos && pos < ui->start + ui->length) { return CHM_RESOLVE_SUCCESS; } /* grab the name */ strLen = _chm_parse_cword(&cur); if (! _chm_parse_UTF8(&cur, strLen, buffer)) return -1; /* check if it is the right name */ /* #ifdef WIN32 if (stricmp(buffer, objPath) > 0) return page; #else if (strcasecmp(buffer, objPath) > 0) return page; #endif */ /* load next value for path */ page = (int)_chm_parse_cword(&cur); } curPage = page; } } /* else, we are confused. give up. */ else return CHM_RESOLVE_FAILURE; } /* didn't find anything. fail. */ return CHM_RESOLVE_FAILURE; }