Leaked source code of windows server 2003
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

790 lines
23 KiB

  1. //+-------------------------------------------------------------------------
  2. //
  3. // Microsoft Windows
  4. // Copyright (C) Microsoft Corporation, 1992 - 2000.
  5. //
  6. // File: cdoc.cxx
  7. //
  8. // Contents: a radically stripped down version of the document class
  9. // that gets rid of the notion of paragragph and maintains only
  10. // information relative to the stream
  11. //
  12. //--------------------------------------------------------------------------
  13. #include <pch.cxx>
  14. #pragma hdrstop
  15. #include <cidebug.hxx>
  16. #include <dynstack.hxx>
  17. #include <cimbmgr.hxx>
  18. #include <propspec.hxx>
  19. #include <vquery.hxx>
  20. #include <pageman.hxx>
  21. #include <dblink.hxx>
  22. #include <imprsnat.hxx>
  23. #include <queryexp.hxx>
  24. #include "whmsg.h"
  25. #include "webdbg.hxx"
  26. #include "cdoc.hxx"
  27. //+-------------------------------------------------------------------------
  28. //
  29. // Function: ComparePositions
  30. //
  31. // Arguments: const void* pPos1 - pointer to first position
  32. // const void* pPos2 - pointer to second position
  33. //
  34. // Synopsis: Comparison function used by qsort to sort positions array
  35. //
  36. //--------------------------------------------------------------------------
  37. int _cdecl ComparePositions(
  38. const void* pPos1,
  39. const void* pPos2 )
  40. {
  41. Position* pp1= (Position*) pPos1;
  42. Position* pp2= (Position*) pPos2;
  43. Win4Assert(0 != pp1 && 0 !=pp2);
  44. if (pp1->GetBegOffset() == pp2->GetBegOffset())
  45. return 0;
  46. else if (pp1->GetBegOffset() < pp2->GetBegOffset())
  47. return -1;
  48. else
  49. return 1;
  50. }
  51. void Hit::Sort()
  52. {
  53. qsort( _aPos, _cPos, sizeof(Position), &ComparePositions );
  54. }
  55. //+-------------------------------------------------------------------------
  56. //
  57. // Member: Hit::Hit, public
  58. //
  59. // Arguments: [aPos] - array of positions
  60. // [cPos] - number of Positions in [aPos]
  61. //
  62. // Synopsis: Create hit from an array of positions
  63. //
  64. //--------------------------------------------------------------------------
  65. Hit::Hit( const Position * aPos, unsigned cPos )
  66. : _cPos(cPos)
  67. {
  68. _aPos = new Position[cPos];
  69. memcpy( _aPos, aPos, sizeof(Position) * cPos );
  70. }
  71. Hit::~Hit()
  72. {
  73. delete[] _aPos;
  74. }
  75. //+-------------------------------------------------------------------------
  76. //
  77. // Member: HitIter::GetPositionCount, public
  78. //
  79. // Synopsis: return number of positions or zero
  80. //
  81. //--------------------------------------------------------------------------
  82. int HitIter::GetPositionCount() const
  83. {
  84. if (_iHit < _pDoc->_cHit && _pDoc->_aHit[_iHit])
  85. return _pDoc->_aHit[_iHit]->GetPositionCount();
  86. return 0;
  87. }
  88. //+-------------------------------------------------------------------------
  89. //
  90. // Member: HitIter::GetPosition, public
  91. //
  92. // Synopsis: return position by value
  93. //
  94. //--------------------------------------------------------------------------
  95. Position HitIter::GetPosition ( int i ) const
  96. {
  97. if ( _iHit < _pDoc->_cHit && _pDoc->_aHit[_iHit] )
  98. return _pDoc->_aHit[_iHit]->GetPos(i);
  99. else
  100. {
  101. Position pos;
  102. return( pos );
  103. }
  104. }
  105. //+-------------------------------------------------------------------------
  106. //
  107. // Member: CDocument::CDocument, public constructor
  108. //
  109. // Arguments: [filename] - the name of the file to hit highlight
  110. // [rank] - the rank of document in the hierarchy - NOT USED
  111. // [rSearch] - ISearch object
  112. // [cmsReadTimeout] - timeout for the initial file read
  113. // [lockSingleThreadedFilter] - lock used for all single
  114. // threaded filters
  115. // [propertyList] - properties to be emitted
  116. // [ulDisplayScript] - setting for displaying scripts
  117. //
  118. // Synopsis: Stream the file in chunk by chunk, scan it for hits,
  119. // and record those positions in the stream matching the restricition.
  120. //
  121. //--------------------------------------------------------------------------
  122. CDocument::CDocument(
  123. WCHAR * filename,
  124. ULONG rank,
  125. ISearchQueryHits & rSearch,
  126. DWORD cmsReadTimeout,
  127. CReleasableLock & lockSingleThreadedFilter,
  128. CEmptyPropertyList & propertyList,
  129. ULONG ulDisplayScript )
  130. : _filename( filename ),
  131. _rank( rank ),
  132. _bufEnd( 0 ),
  133. _iChunkHint( 0 ),
  134. _cHit( 0 ),
  135. _rSearch( rSearch ),
  136. _cmsReadTimeout( cmsReadTimeout ),
  137. _lockSingleThreadedFilter( lockSingleThreadedFilter )
  138. {
  139. BOOL noHits = FALSE;
  140. //
  141. // cut away anything after the non-drive colon
  142. // like in c:\wzmail\foo.fld:12.wzm
  143. //
  144. WCHAR* pChar = _filename;
  145. if ( _filename[1] == L':')
  146. pChar += 2;
  147. while (*pChar != 0 && *pChar != L':')
  148. pChar++;
  149. if(*pChar == L':')
  150. *pChar = 0;
  151. //
  152. // allocate a buffer to hold the file
  153. //
  154. AllocBuffer();
  155. //
  156. // attach to IFilter
  157. //
  158. BOOL fKnownFilter = BindToFilter();
  159. // Check if this file's extension has a script mapping (if necessary)
  160. BOOL fHasScriptMap = FALSE;
  161. if ( ( DISPLAY_SCRIPT_NONE == ulDisplayScript ) ||
  162. ( ( DISPLAY_SCRIPT_KNOWN_FILTER == ulDisplayScript ) &&
  163. ( !fKnownFilter ) ) )
  164. {
  165. WCHAR *pwcExt = wcsrchr( _filename, L'.' );
  166. webDebugOut(( DEB_ITRACE, "extension: '%ws'\n", pwcExt ));
  167. if ( 0 != pwcExt )
  168. {
  169. //
  170. // .asp files include .inc files. .inc files don't have a script
  171. // map but they contain script. I'm not aware of a good way to
  172. // enumerate all possible include file extensions for asp.
  173. //
  174. if ( !_wcsicmp( pwcExt, L".inc" ) )
  175. fHasScriptMap = TRUE;
  176. else
  177. {
  178. //
  179. // Must be system to read the metabase
  180. //
  181. CImpersonateSystem system;
  182. CMetaDataMgr mdMgr( TRUE, W3VRoot );
  183. fHasScriptMap = mdMgr.ExtensionHasScriptMap( pwcExt );
  184. }
  185. }
  186. }
  187. webDebugOut(( DEB_ITRACE,
  188. "fHasScriptMap %d, fKnownFilter %d, ulDisplayScript %d\n",
  189. fHasScriptMap, fKnownFilter, ulDisplayScript ));
  190. if ( fHasScriptMap )
  191. {
  192. if ( ( DISPLAY_SCRIPT_NONE == ulDisplayScript ) ||
  193. ( ( DISPLAY_SCRIPT_KNOWN_FILTER == ulDisplayScript ) &&
  194. ( !fKnownFilter ) ) )
  195. {
  196. THROW( CException( MSG_WEBHITS_PATH_INVALID ) );
  197. }
  198. }
  199. //
  200. // Initialize IFilter. Pass the list of properties to be emitted, since
  201. // some other properties may have sensitive information (eg passwords in
  202. // vbscript code in .asp files).
  203. //
  204. // First count how many properties exist.
  205. ULONG cProps = propertyList.GetCount();
  206. // Copy the properties
  207. CDbColumns aSpecs( cProps );
  208. CDbColId prop;
  209. for ( unsigned iProp = 0; iProp < cProps; iProp++ )
  210. aSpecs.Add( prop, iProp );
  211. typedef CPropEntry * PCPropEntry;
  212. XArray<PCPropEntry> xapPropEntries(cProps);
  213. SCODE sc = propertyList.GetAllEntries(xapPropEntries.GetPointer(), cProps);
  214. Win4Assert(S_OK == sc);
  215. if (FAILED (sc))
  216. THROW (CException(sc));
  217. PCPropEntry *apPropEntries = xapPropEntries.GetPointer();
  218. for (ULONG i = 0; i < cProps; i++)
  219. {
  220. CDbColId * pcol = (CDbColId *) &aSpecs.Get( i );
  221. *pcol = apPropEntries[i]->PropSpec();
  222. if ( !pcol->IsValid())
  223. THROW (CException(E_OUTOFMEMORY));
  224. }
  225. webDebugOut(( DEB_ITRACE, "%d properties being processed\n", cProps ));
  226. ULONG ulFlags;
  227. sc = _xFilter->Init( IFILTER_INIT_CANON_PARAGRAPHS |
  228. IFILTER_INIT_CANON_HYPHENS |
  229. IFILTER_INIT_APPLY_INDEX_ATTRIBUTES,
  230. cProps,
  231. (FULLPROPSPEC *) aSpecs.GetColumnsArray(),
  232. &ulFlags );
  233. if (FAILED (sc))
  234. THROW (CException(sc));
  235. //
  236. // pull the contents of the file into the buffer
  237. //
  238. ReadFile();
  239. // Some broken filters don't work right if you Init() them twice, so
  240. // throw away the IFilter, and get it again.
  241. _xFilter.Free();
  242. BindToFilter();
  243. sc = _xFilter->Init( IFILTER_INIT_CANON_PARAGRAPHS |
  244. IFILTER_INIT_CANON_HYPHENS |
  245. IFILTER_INIT_APPLY_INDEX_ATTRIBUTES,
  246. cProps,
  247. (FULLPROPSPEC *) aSpecs.GetColumnsArray(),
  248. &ulFlags );
  249. if (FAILED (sc))
  250. THROW (CException(sc));
  251. //
  252. // attach to ISearchQueryHits, which will find the hits
  253. //
  254. sc = _rSearch.Init( _xFilter.GetPointer(), ulFlags );
  255. if (FAILED (sc))
  256. {
  257. if ( QUERY_E_INVALIDRESTRICTION != sc )
  258. THROW (CException(sc));
  259. // we can still show the file
  260. noHits = TRUE;
  261. }
  262. //
  263. // pull up all the hits
  264. //
  265. TRY
  266. {
  267. if (!noHits)
  268. {
  269. ULONG count;
  270. FILTERREGION* aRegion;
  271. SCODE sc = _rSearch.NextHitOffset( &count, &aRegion );
  272. while ( S_OK == sc )
  273. {
  274. XCoMem<FILTERREGION> xRegion( aRegion );
  275. webDebugOut(( DEB_ITRACE,
  276. "CDOCUMENT: next hit: count %d, chunk %d offset %d, ext %d\n",
  277. count,
  278. aRegion[0].idChunk,
  279. aRegion[0].cwcStart,
  280. aRegion[0].cwcExtent ));
  281. CDynArrayInPlace<Position> aPos( count );
  282. //
  283. // get the positions in the hit
  284. //
  285. for (unsigned i = 0; i < count; i++)
  286. {
  287. aPos[i] = RegionToPos( aRegion [i] );
  288. webDebugOut(( DEB_ITRACE,
  289. " region %d, start %d, length %d\n",
  290. i,
  291. aPos[i].GetBegOffset(),
  292. aPos[i].GetLength() ));
  293. }
  294. xRegion.Free();
  295. XPtr<Hit> xHit( new Hit( aPos.GetPointer(), count ) );
  296. _aHit[_cHit] = xHit.GetPointer();
  297. _cHit++;
  298. xHit.Acquire();
  299. sc = _rSearch.NextHitOffset( &count, &aRegion );
  300. }
  301. if ( FAILED( sc ) )
  302. THROW( CException( sc ) );
  303. }
  304. }
  305. CATCH( CException, e )
  306. {
  307. FreeHits();
  308. RETHROW();
  309. }
  310. END_CATCH;
  311. // done with the filter
  312. _xFilter.Free();
  313. if ( _lockSingleThreadedFilter.IsHeld() )
  314. _lockSingleThreadedFilter.Release();
  315. } //CDocument
  316. //+-------------------------------------------------------------------------
  317. //
  318. // Member: CDocument::~CDocument, public
  319. //
  320. // Synopsis: Free CDocument
  321. //
  322. //--------------------------------------------------------------------------
  323. CDocument::~CDocument()
  324. {
  325. FreeHits();
  326. } //~CDocument
  327. //+-------------------------------------------------------------------------
  328. //
  329. // Member: CDocument::Free, public
  330. //
  331. // Synopsis: Free CDocument storage
  332. //
  333. //--------------------------------------------------------------------------
  334. void CDocument::FreeHits()
  335. {
  336. //
  337. // walk through _aHit, deleting each Positions array that the
  338. // cells are pointing to
  339. //
  340. for ( unsigned i = 0; i < _cHit; i++ )
  341. {
  342. delete _aHit[i];
  343. _aHit[i] = 0;
  344. }
  345. _cHit = 0;
  346. } //Free
  347. //+-------------------------------------------------------------------------
  348. //
  349. // Member: CDocument::RegionToPos, public
  350. //
  351. // Synopsis: Convert a FILTERREGION to a position
  352. //
  353. //--------------------------------------------------------------------------
  354. Position CDocument::RegionToPos(
  355. FILTERREGION& region )
  356. {
  357. //
  358. // Use a linear search here. In profile runs this has never shown
  359. // up as a problem. Fix if this changes.
  360. //
  361. ULONG offset = ULONG (-1);
  362. //
  363. // check whether we're not trying to access an illegal chunk
  364. //
  365. if (_iChunkHint >= _chunkCount || _chunk[_iChunkHint].ChunkId() !=
  366. region.idChunk )
  367. {
  368. _iChunkHint = 0;
  369. while ( _iChunkHint < _chunkCount && _chunk[_iChunkHint].ChunkId() <
  370. region.idChunk )
  371. {
  372. _iChunkHint++;
  373. }
  374. if (_iChunkHint >= _chunkCount || _chunk[_iChunkHint].ChunkId()
  375. != region.idChunk)
  376. {
  377. return Position();
  378. }
  379. }
  380. //
  381. // _iChunkHint now contains the index of the appropriate chunk in the
  382. // chunk array
  383. //
  384. Win4Assert ( _iChunkHint < _chunkCount );
  385. Win4Assert ( _chunk[_iChunkHint].ChunkId() == region.idChunk );
  386. //
  387. // offset now stores the linear offset of the position from the
  388. // beginning of the stream/buffer
  389. //
  390. offset = _chunk[_iChunkHint].Offset() + region.cwcStart;
  391. return Position (offset,region.cwcExtent );
  392. } //RegionToPos
  393. //+-------------------------------------------------------------------------
  394. //
  395. // Member: CDocument::AllocBuffer, public
  396. //
  397. // Synopsis: Allocate buffer for file text
  398. //
  399. //--------------------------------------------------------------------------
  400. void CDocument::AllocBuffer()
  401. {
  402. HANDLE hFile = CreateFile( _filename,
  403. GENERIC_READ,
  404. FILE_SHARE_READ,
  405. 0, // security
  406. OPEN_EXISTING,
  407. FILE_ATTRIBUTE_NORMAL,
  408. 0 ); // template
  409. if ( INVALID_HANDLE_VALUE == hFile )
  410. THROW( CException() );
  411. ULONG cbBuf = GetFileSize( hFile, 0 );
  412. CloseHandle( hFile );
  413. // Allow extra room for custom properties to be emitted from the
  414. // filter, plus the conversion to unicode
  415. _xBuffer.Init( cbBuf + cbBuf / 2 );
  416. } //AllocBuffer
  417. //+-------------------------------------------------------------------------
  418. //
  419. // Member: CDocument::BindToFilter, public
  420. //
  421. // Synopsis: Bind to appropriate filter for the CDocument
  422. //
  423. // Returns: TRUE if an appropriate filter was found
  424. // FALSE if defaulted to the text filter
  425. //
  426. //--------------------------------------------------------------------------
  427. BOOL CDocument::BindToFilter()
  428. {
  429. //
  430. // Bind to the filter interface -- try free threaded first. If the
  431. // filter isn't thread-safe, grab the lock and get the filter.
  432. //
  433. SCODE sc = LoadBHIFilter( _filename, 0, _xFilter.GetQIPointer(), FALSE );
  434. // Is the filter not thread safe? If so, get the lock to protect
  435. // the filter. No checking is done to see that this particular
  436. // filter is in use -- just that some non-thread-safe filter is in use.
  437. if ( S_FALSE == sc )
  438. {
  439. // If the lock isn't held yet, get it (BindToFilter is called
  440. // twice by CDocument's constructor, so check IsHeld())
  441. if ( !_lockSingleThreadedFilter.IsHeld() )
  442. _lockSingleThreadedFilter.Request();
  443. // retry to load the filter as single-threaded
  444. sc = LoadBHIFilter( _filename, 0, _xFilter.GetQIPointer(), TRUE );
  445. }
  446. BOOL fFoundFilter = TRUE;
  447. if ( FAILED(sc) )
  448. {
  449. sc = LoadTextFilter( _filename, _xFilter.GetPPointer() );
  450. if (FAILED(sc))
  451. THROW (CException(sc));
  452. fFoundFilter = FALSE;
  453. }
  454. return fFoundFilter;
  455. } //BindToFilter
  456. //+-------------------------------------------------------------------------
  457. //
  458. // Function: GetThreadTime
  459. //
  460. // Synopsis: Gets the current total cpu usage for the thread
  461. //
  462. //--------------------------------------------------------------------------
  463. LONGLONG GetThreadTime()
  464. {
  465. FILETIME ftDummy1, ftDummy2;
  466. LONGLONG llUser, llKernel;
  467. Win4Assert( sizeof(LONGLONG) == sizeof(FILETIME) );
  468. GetThreadTimes( GetCurrentThread(),
  469. &ftDummy1, // Creation time
  470. &ftDummy2, // Exit time
  471. (FILETIME *) &llUser, // user mode time
  472. (FILETIME *) &llKernel ); // kernel mode tiem
  473. return llKernel + llUser;
  474. } //GetThreadTime
  475. //+-------------------------------------------------------------------------
  476. //
  477. // Member: CDocument::ReadFile, public
  478. //
  479. // Synopsis: Read file into buffer using the filter
  480. //
  481. //--------------------------------------------------------------------------
  482. void CDocument::ReadFile()
  483. {
  484. // get the maximum cpu time in 100s of nano seconds.
  485. LONGLONG llLimitCpuTime = _cmsReadTimeout * 1000 * 10000;
  486. llLimitCpuTime += GetThreadTime();
  487. ULONG cwcSoFar = 0;
  488. int cChunk = 0;
  489. BOOL fSeenProp = FALSE;
  490. STAT_CHUNK statChunk;
  491. SCODE sc = _xFilter->GetChunk ( &statChunk );
  492. //
  493. // Take them into account at some point
  494. // to test more complicated chunking
  495. //
  496. //
  497. // keep getting chunks of the file, placing them in the buffer,
  498. // and setting the chunk offset markers that will be used to
  499. // interpolate the buffer
  500. //
  501. while ( SUCCEEDED(sc)
  502. || FILTER_E_LINK_UNAVAILABLE == sc
  503. || FILTER_E_EMBEDDING_UNAVAILABLE == sc
  504. || FILTER_E_NO_TEXT == sc )
  505. {
  506. //
  507. // Eliminate all chunks with idChunkSource 0 right here - these
  508. // cannot be hit highlighted.
  509. // Also eliminate all CHUNK_VALUE chunks.
  510. //
  511. if ( SUCCEEDED( sc ) && (statChunk.flags & CHUNK_TEXT) && (0 != statChunk.idChunkSource) )
  512. {
  513. //
  514. // set markers
  515. //
  516. Win4Assert ( cChunk == 0 || statChunk.idChunk >
  517. _chunk [cChunk - 1].ChunkId() );
  518. //
  519. // If there was an end of sentence or paragraph or chapter, we
  520. // should introduce an appropriate spacing character.
  521. //
  522. if ( statChunk.breakType != CHUNK_NO_BREAK &&
  523. cwcSoFar < _xBuffer.Count() )
  524. {
  525. switch (statChunk.breakType)
  526. {
  527. case CHUNK_EOW:
  528. case CHUNK_EOS:
  529. _xBuffer[cwcSoFar++] = L' '; // introduce a space character
  530. break;
  531. case CHUNK_EOP:
  532. case CHUNK_EOC:
  533. _xBuffer[cwcSoFar++] = UNICODE_PARAGRAPH_SEPARATOR;
  534. break;
  535. }
  536. }
  537. //
  538. // The Offset into the stream depends on whether this is an
  539. // 'original' chunk or not
  540. //
  541. CCiPropSpec* pProp = (CCiPropSpec*) &statChunk.attribute;
  542. webDebugOut(( DEB_ITRACE,
  543. "Chunk %d, Source %d, Contents %d, start %d, cwc %d\n",
  544. statChunk.idChunk,
  545. statChunk.idChunkSource,
  546. pProp->IsContents(),
  547. statChunk.cwcStartSource,
  548. statChunk.cwcLenSource ));
  549. if ( (statChunk.idChunk == statChunk.idChunkSource) &&
  550. pProp->IsContents() )
  551. {
  552. _chunk[cChunk].SetChunkId( statChunk.idChunk );
  553. _chunk[cChunk].SetOffset( cwcSoFar );
  554. cChunk++;
  555. #if 0
  556. }
  557. else if ( statChunk.idChunk != statChunk.idChunkSource )
  558. {
  559. _chunk [cChunk].SetChunkId (statChunk.idChunk);
  560. //
  561. // we have to first find the offset of the source chunk
  562. //
  563. for (int i=cChunk-1;i>=0;i--)
  564. {
  565. if (_chunk[i].ChunkId() == statChunk.idChunkSource)
  566. {
  567. _chunk[cChunk].SetOffset(_chunk[i].Offset()+statChunk.cwcStartSource);
  568. break;
  569. }
  570. }
  571. cChunk++;
  572. }
  573. //
  574. // if the chunk is a contents chunk and idChunkSrc = idChunk,
  575. // then pull it in
  576. //
  577. if ( (statChunk.idChunk == statChunk.idChunkSource) &&
  578. pProp->IsContents() )
  579. {
  580. #endif
  581. webDebugOut(( DEB_ITRACE, "CDOC: markers: chunk %d offset %d\n",
  582. _chunk[cChunk-1].ChunkId(),
  583. _chunk[cChunk-1].Offset() ));
  584. //
  585. // push the text into memory
  586. //
  587. do
  588. {
  589. ULONG cwcThis = _xBuffer.Count() - cwcSoFar;
  590. if ( 0 == cwcThis )
  591. break;
  592. sc = _xFilter->GetText( &cwcThis,
  593. _xBuffer.GetPointer() + cwcSoFar );
  594. if (SUCCEEDED(sc))
  595. {
  596. cwcSoFar += cwcThis;
  597. }
  598. }
  599. while (SUCCEEDED(sc));
  600. }
  601. } // If SUCCEEDED( sc )
  602. if ( GetThreadTime() > llLimitCpuTime )
  603. {
  604. webDebugOut(( DEB_ERROR, "Webhits took too long. Timeout\n" ));
  605. THROW( CException( MSG_WEBHITS_TIMEOUT ) );
  606. }
  607. //
  608. // next chunk, please
  609. //
  610. sc = _xFilter->GetChunk ( &statChunk );
  611. }
  612. _bufEnd = _xBuffer.GetPointer() + cwcSoFar;
  613. _chunkCount = cChunk;
  614. } //ReadFile
  615. WCHAR* CDocument::GetWritablePointerToOffset(
  616. long offset )
  617. {
  618. if (offset >= 0)
  619. {
  620. if (_xBuffer.GetPointer() + offset < _bufEnd)
  621. return _xBuffer.GetPointer() + offset;
  622. else
  623. return _bufEnd;
  624. }
  625. else
  626. {
  627. return _xBuffer.GetPointer();
  628. }
  629. } //GetWritablePointerToOffset
  630. //+-------------------------------------------------------------------------
  631. //
  632. // Member: CDocument::GetPointerToOffset, public
  633. //
  634. // Arguments: [offset] - the offset in the stream that we want a pointer to
  635. //
  636. // Synopsis: Return a constant pointer to a specific offset in the buffer
  637. //
  638. //--------------------------------------------------------------------------
  639. const WCHAR* CDocument::GetPointerToOffset(long offset)
  640. {
  641. return (const WCHAR *) GetWritablePointerToOffset(offset);
  642. } //GetPointerToOffset