Source code of Windows XP (NT5)
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.

707 lines
20 KiB

  1. //+---------------------------------------------------------------------------
  2. //
  3. // Microsoft Windows
  4. // Copyright (C) Microsoft Corporation, 1996 - 2000.
  5. //
  6. // File: ixsquery.cxx
  7. //
  8. // Contents: Query SSO active query state class
  9. //
  10. // History: 29 Oct 1996 Alanw Created
  11. //
  12. //----------------------------------------------------------------------------
  13. #include "pch.cxx"
  14. #pragma hdrstop
  15. #include "ixsso.hxx"
  16. #include "ssodebug.hxx"
  17. #include <strrest.hxx>
  18. #include <strsort.hxx>
  19. #include <qlibutil.hxx>
  20. #if CIDBG
  21. #include <stdio.h>
  22. #endif // CIDBG
  23. #include <initguid.h>
  24. #include <nlimport.h>
  25. static const DBID dbcolNull = { {0,0,0,{0,0,0,0,0,0,0,0}},DBKIND_GUID_PROPID,0};
  26. static const GUID guidQueryExt = DBPROPSET_QUERYEXT;
  27. static const GUID guidRowsetProps = DBPROPSET_ROWSET;
  28. //+---------------------------------------------------------------------------
  29. //
  30. // Member: CixssoQuery::GetDefaultCatalog - private inline
  31. //
  32. // Synopsis: Initializes the _pwszCatalog member with the default catalog.
  33. //
  34. // Arguments: NONE
  35. //
  36. // Notes: The IS 2.0 implementation of ISCC::GetDefaultCatalog has two
  37. // flaws that are worked around here. It should return the size
  38. // of the required string when zero is passed in as the input
  39. // length, and it should null terminate the output string.
  40. //
  41. // History: 18 Dec 1996 AlanW Created
  42. // 21 Oct 1997 AlanW Modified to use ISimpleCommandCreator
  43. //
  44. //----------------------------------------------------------------------------
  45. inline void CixssoQuery::GetDefaultCatalog( )
  46. {
  47. ixssoDebugOut(( DEB_ITRACE, "Using default catalog\n" ));
  48. ULONG cchRequired = 0;
  49. SCODE sc = _xCmdCreator->GetDefaultCatalog(0, 0, &cchRequired);
  50. if ( cchRequired == 0 )
  51. {
  52. // IS 2.0 doesn't return required path length
  53. cchRequired = MAX_PATH;
  54. }
  55. else if ( cchRequired > MAX_PATH )
  56. {
  57. THROW( CException(E_INVALIDARG) );
  58. }
  59. cchRequired++; // make room for termination
  60. XArray<WCHAR> pwszCat ( cchRequired );
  61. sc = _xCmdCreator->GetDefaultCatalog(pwszCat.GetPointer(), cchRequired, &cchRequired);
  62. if (FAILED(sc))
  63. THROW( CException(sc) );
  64. Win4Assert( 0 == _pwszCatalog );
  65. _pwszCatalog = pwszCat.Acquire();
  66. // IS 2.0 does not transfer the null character at the end of the string.
  67. _pwszCatalog[ cchRequired ] = L'\0';
  68. return;
  69. }
  70. //+---------------------------------------------------------------------------
  71. //
  72. // Function: ParseCatalogs - public
  73. //
  74. // Synopsis: Parse a comma-separated catalog string, return the count
  75. // of catalogs and the individual catalog and machine names.
  76. //
  77. // Arguments: [pwszCatalog] - input catalog string
  78. // [aCatalogs] - array for returned catalog names
  79. // [aMachines] - array for returned machine names
  80. //
  81. // Returns: ULONG - number of catalogs
  82. //
  83. // Notes:
  84. //
  85. // History: 18 Jun 1997 Alanw Created
  86. //
  87. //----------------------------------------------------------------------------
  88. ULONG ParseCatalogs( WCHAR * pwszCatalog,
  89. CDynArray<WCHAR> & aCatalogs,
  90. CDynArray<WCHAR> & aMachines )
  91. {
  92. ULONG cCatalogs = 0;
  93. while ( 0 != *pwszCatalog )
  94. {
  95. // eat space and commas
  96. while ( L' ' == *pwszCatalog || L',' == *pwszCatalog )
  97. pwszCatalog++;
  98. if ( 0 == *pwszCatalog )
  99. break;
  100. WCHAR awchCat[MAX_PATH];
  101. WCHAR * pwszOut = awchCat;
  102. // is this a quoted path?
  103. if ( L'"' == *pwszCatalog )
  104. {
  105. pwszCatalog++;
  106. while ( 0 != *pwszCatalog &&
  107. L'"' != *pwszCatalog &&
  108. pwszOut < &awchCat[MAX_PATH] )
  109. *pwszOut++ = *pwszCatalog++;
  110. if ( L'"' != *pwszCatalog )
  111. THROW( CIxssoException( MSG_IXSSO_BAD_CATALOG, 0 ) );
  112. pwszCatalog++;
  113. *pwszOut++ = 0;
  114. }
  115. else
  116. {
  117. while ( 0 != *pwszCatalog &&
  118. L',' != *pwszCatalog &&
  119. pwszOut < &awchCat[MAX_PATH] )
  120. *pwszOut++ = *pwszCatalog++;
  121. if ( pwszOut >= &awchCat[MAX_PATH] )
  122. THROW( CIxssoException( MSG_IXSSO_BAD_CATALOG, 0 ) );
  123. // back up over trailing spaces
  124. while ( L' ' == * (pwszOut - 1) )
  125. pwszOut--;
  126. *pwszOut++ = 0;
  127. }
  128. XPtrST<WCHAR> xpCat;
  129. XPtrST<WCHAR> xpMach;
  130. SCODE sc = ParseCatalogURL( awchCat, xpCat, xpMach );
  131. if (FAILED(sc) || xpCat.IsNull() )
  132. THROW( CIxssoException( MSG_IXSSO_BAD_CATALOG, 0 ) );
  133. aCatalogs.Add(xpCat.GetPointer(), cCatalogs);
  134. xpCat.Acquire();
  135. aMachines.Add(xpMach.GetPointer(), cCatalogs);
  136. xpMach.Acquire();
  137. cCatalogs++;
  138. }
  139. if ( 0 == cCatalogs )
  140. THROW( CIxssoException( MSG_IXSSO_BAD_CATALOG, 0 ) );
  141. return cCatalogs;
  142. } //ParseCatalogs
  143. //+---------------------------------------------------------------------------
  144. //
  145. // Member: CixssoQuery::GetDialect - private
  146. //
  147. // Synopsis: Parses the dialect string and returns ISQLANG_V*
  148. //
  149. // Returns: The dialect identifier
  150. //
  151. // History: 19 Nov 1997 dlee Created
  152. // 03 Dec 1998 KrishnaN Defaulting to version 2
  153. //
  154. //----------------------------------------------------------------------------
  155. ULONG CixssoQuery::GetDialect()
  156. {
  157. if ( 0 == _pwszDialect )
  158. return ISQLANG_V2;
  159. ULONG ul = _wtoi( _pwszDialect );
  160. if ( ul < ISQLANG_V1 || ul > ISQLANG_V2 )
  161. return ISQLANG_V2;
  162. return ul;
  163. } //GetDialect
  164. //+---------------------------------------------------------------------------
  165. //
  166. // Member: CixssoQuery::ExecuteQuery - private
  167. //
  168. // Synopsis: Executes the query and builds an IRowset or IRowsetScroll
  169. // as necessary.
  170. //
  171. // Arguments: NONE
  172. //
  173. // History: 29 Oct 1996 Alanw Created
  174. //
  175. //----------------------------------------------------------------------------
  176. void CixssoQuery::ExecuteQuery( )
  177. {
  178. Win4Assert( 0 == _pIRowset ); // Should not have executed query
  179. //
  180. // Setup the variables needed to execute this query; including:
  181. //
  182. // Query
  183. // MaxRecords
  184. // SortBy
  185. //
  186. ixssoDebugOut(( DEB_TRACE, "ExecuteQuery:\n" ));
  187. ixssoDebugOut(( DEB_TRACE, "\tQuery = '%ws'\n", _pwszRestriction ));
  188. if ( 0 == _pwszRestriction || 0 == *_pwszRestriction )
  189. {
  190. THROW( CIxssoException(MSG_IXSSO_MISSING_RESTRICTION, 0) );
  191. }
  192. ixssoDebugOut(( DEB_TRACE, "\tMaxRecords = %d\n", _maxResults ));
  193. ixssoDebugOut(( DEB_TRACE, "\tFirstRowss = %d\n", _cFirstRows ));
  194. //
  195. // Get the columns in the query
  196. //
  197. ixssoDebugOut(( DEB_TRACE, "\tColumns = '%ws'\n", _pwszColumns ));
  198. if ( 0 == _pwszColumns || 0 == *_pwszColumns )
  199. {
  200. THROW( CIxssoException(MSG_IXSSO_MISSING_OUTPUTCOLUMNS, 0) );
  201. }
  202. if ( 0 != _pwszGroup && 0 != *_pwszGroup && _fSequential )
  203. {
  204. // Grouped queries are always non-sequential.
  205. _fSequential = FALSE;
  206. }
  207. //
  208. // Convert the textual form of the restriction, output columns and
  209. // sort columns into a DBCOMMANDTREE.
  210. //
  211. if (InvalidLCID == _lcid)
  212. {
  213. THROW( CIxssoException(MSG_IXSSO_INVALID_LOCALE, 0) );
  214. }
  215. ULONG ulDialect = GetDialect();
  216. CTextToTree textToTree( _pwszRestriction,
  217. ulDialect,
  218. _pwszColumns,
  219. GetColumnMapper(),
  220. _lcid,
  221. _pwszSort,
  222. _pwszGroup,
  223. 0,
  224. _maxResults,
  225. _cFirstRows,
  226. TRUE // Keep the friendly column names
  227. );
  228. CDbCmdTreeNode * pDbCmdTree = (CDbCmdTreeNode *) (void *) textToTree.FormFullTree();
  229. XPtr<CDbCmdTreeNode> xDbCmdTree( pDbCmdTree );
  230. CDynArray<WCHAR> apCatalog;
  231. CDynArray<WCHAR> apMachine;
  232. //
  233. // Get the location of the catalog. Use the default if the catalog
  234. // property is not set.
  235. //
  236. if ( 0 == _pwszCatalog )
  237. {
  238. GetDefaultCatalog();
  239. if ( 0 == _pwszCatalog )
  240. THROW( CIxssoException(MSG_IXSSO_NO_SUCH_CATALOG, 0) );
  241. }
  242. ixssoDebugOut(( DEB_TRACE, "\tCatalog = '%ws'\n", _pwszCatalog ));
  243. ULONG cCatalogs = ParseCatalogs( _pwszCatalog, apCatalog, apMachine );
  244. //
  245. // Get the scope specification(s) for the query
  246. // CiScope
  247. // CiFlags
  248. //
  249. Win4Assert( _cScopes <= _apwszScope.Size() );
  250. for ( unsigned i = 0; i < _cScopes; i++)
  251. {
  252. # if CIDBG
  253. char szIdx[10] = "";
  254. if (_cScopes > 1)
  255. sprintf( szIdx, " [%d]", i );
  256. # endif // CIDBG
  257. ixssoDebugOut(( DEB_TRACE, "\tCiScope%s = '%ws'\n", szIdx, _apwszScope[i] ));
  258. //
  259. // Get the query flags.
  260. //
  261. if (i >= _aulDepth.Count())
  262. _aulDepth[i] = QUERY_DEEP;
  263. if ( IsAVirtualPath( _apwszScope[i] ) )
  264. _aulDepth[i] |= QUERY_VIRTUAL_PATH;
  265. ixssoDebugOut(( DEB_TRACE, "\tCiFlags%s = '%ws'\n", szIdx,
  266. _aulDepth.Get(i) & QUERY_DEEP ? L"DEEP":L"SHALLOW" ));
  267. }
  268. //
  269. // We've setup all the parameters to run the query. Run the query
  270. // now.
  271. //
  272. IUnknown * pIUnknown;
  273. ICommand *pCommand = 0;
  274. SCODE sc = _xCmdCreator->CreateICommand(&pIUnknown, 0);
  275. if (SUCCEEDED (sc))
  276. {
  277. XInterface<IUnknown> xUnk( pIUnknown );
  278. sc = pIUnknown->QueryInterface(IID_ICommand, (void **)&pCommand);
  279. }
  280. if ( 0 == pCommand )
  281. {
  282. THROW( CIxssoException(sc, 0) );
  283. }
  284. XInterface<ICommand> xICommand(pCommand);
  285. if (0 == _cScopes)
  286. {
  287. //
  288. // Default path: search everywhere
  289. //
  290. const WCHAR * pwszPath = L"\\";
  291. DWORD dwDepth = QUERY_DEEP;
  292. if ( 1 == cCatalogs )
  293. {
  294. SetScopeProperties( pCommand,
  295. 1,
  296. &pwszPath,
  297. &dwDepth,
  298. apCatalog.GetPointer(),
  299. apMachine.GetPointer() );
  300. }
  301. else
  302. {
  303. SetScopeProperties( pCommand,
  304. 1,
  305. &pwszPath,
  306. &dwDepth,
  307. 0,
  308. 0 );
  309. SetScopeProperties( pCommand,
  310. cCatalogs,
  311. 0,
  312. 0,
  313. apCatalog.GetPointer(),
  314. apMachine.GetPointer() );
  315. }
  316. }
  317. else
  318. {
  319. SetScopeProperties( pCommand,
  320. _cScopes,
  321. (WCHAR const * const *)_apwszScope.GetPointer(),
  322. _aulDepth.GetPointer(),
  323. 0,
  324. 0 );
  325. SetScopeProperties( pCommand,
  326. cCatalogs,
  327. 0,
  328. 0,
  329. apCatalog.GetPointer(),
  330. apMachine.GetPointer() );
  331. }
  332. ICommandTree * pICmdTree = 0;
  333. sc = xICommand->QueryInterface(IID_ICommandTree, (void **)&pICmdTree);
  334. if (FAILED (sc) )
  335. {
  336. THROW( CException( QUERY_EXECUTE_FAILED ) );
  337. }
  338. DBCOMMANDTREE * pDbCommandTree = pDbCmdTree->CastToStruct();
  339. sc = pICmdTree->SetCommandTree(&pDbCommandTree, DBCOMMANDREUSE_NONE, FALSE);
  340. pICmdTree->Release();
  341. if ( FAILED(sc) )
  342. {
  343. THROW( CException(sc) );
  344. }
  345. xDbCmdTree.Acquire();
  346. //
  347. // Set properties on the command object.
  348. //
  349. const unsigned MAX_PROPS = 5;
  350. DBPROPSET aPropSet[MAX_PROPS];
  351. DBPROP aProp[MAX_PROPS];
  352. ULONG cProp = 0;
  353. ULONG iHitCountProp = MAX_PROPS;
  354. // Set the property that says whether we want to enumerate
  355. Win4Assert( cProp < MAX_PROPS );
  356. ixssoDebugOut(( DEB_TRACE, "\tUseContentIndex = %s\n",
  357. _fAllowEnumeration ? "FALSE" : "TRUE" ));
  358. aProp[cProp].dwPropertyID = DBPROP_USECONTENTINDEX;
  359. aProp[cProp].dwOptions = DBPROPOPTIONS_OPTIONAL;
  360. aProp[cProp].dwStatus = 0; // Ignored
  361. aProp[cProp].colid = dbcolNull;
  362. aProp[cProp].vValue.vt = VT_BOOL;
  363. aProp[cProp].vValue.boolVal = _fAllowEnumeration ? VARIANT_FALSE :
  364. VARIANT_TRUE;
  365. aPropSet[cProp].rgProperties = &aProp[cProp];
  366. aPropSet[cProp].cProperties = 1;
  367. aPropSet[cProp].guidPropertySet = guidQueryExt;
  368. cProp++;
  369. // Set the property for retrieving hit count
  370. Win4Assert( cProp < MAX_PROPS );
  371. ixssoDebugOut(( DEB_TRACE, "\tNlHitCount = %s\n",
  372. ( _dwOptimizeFlags & eOptHitCount ) ? "TRUE" :
  373. "FALSE" ));
  374. aProp[cProp].dwPropertyID = NLDBPROP_GETHITCOUNT;
  375. aProp[cProp].dwOptions = DBPROPOPTIONS_OPTIONAL;
  376. aProp[cProp].dwStatus = 0; // Ignored
  377. aProp[cProp].colid = dbcolNull;
  378. aProp[cProp].vValue.vt = VT_BOOL;
  379. aProp[cProp].vValue.boolVal =
  380. ( _dwOptimizeFlags & eOptHitCount ) ? VARIANT_TRUE :
  381. VARIANT_FALSE;
  382. aPropSet[cProp].rgProperties = &aProp[cProp];
  383. aPropSet[cProp].cProperties = 1;
  384. aPropSet[cProp].guidPropertySet = DBPROPSET_NLCOMMAND;
  385. iHitCountProp = cProp;
  386. cProp++;
  387. if ( _dwOptimizeFlags & eOptPerformance )
  388. {
  389. // Set the property for magically fast queries
  390. Win4Assert( cProp < MAX_PROPS );
  391. ixssoDebugOut(( DEB_TRACE, "\tCiDeferNonIndexedTrimming = TRUE\n" ));
  392. aProp[cProp].dwPropertyID = DBPROP_DEFERNONINDEXEDTRIMMING;
  393. aProp[cProp].dwOptions = DBPROPOPTIONS_OPTIONAL;
  394. aProp[cProp].dwStatus = 0; // Ignored
  395. aProp[cProp].colid = dbcolNull;
  396. aProp[cProp].vValue.vt = VT_BOOL;
  397. aProp[cProp].vValue.boolVal = VARIANT_TRUE;
  398. aPropSet[cProp].rgProperties = &aProp[cProp];
  399. aPropSet[cProp].cProperties = 1;
  400. aPropSet[cProp].guidPropertySet = guidQueryExt;
  401. cProp++;
  402. }
  403. // set the start hit property if it is set
  404. if ( _StartHit.Get() )
  405. {
  406. // Set the start hit property
  407. Win4Assert( cProp < MAX_PROPS );
  408. ixssoDebugOut(( DEB_TRACE, "\tStartHit = %x\n", _StartHit.Get() ));
  409. aProp[cProp].dwPropertyID = NLDBPROP_STARTHIT;
  410. aProp[cProp].dwOptions = 0;
  411. aProp[cProp].dwStatus = 0; // Ignored
  412. aProp[cProp].colid = dbcolNull;
  413. V_VT(&(aProp[cProp].vValue)) = VT_ARRAY | VT_I4;
  414. V_ARRAY(&(aProp[cProp].vValue)) = _StartHit.Get();
  415. aPropSet[cProp].rgProperties = &aProp[cProp];
  416. aPropSet[cProp].cProperties = 1;
  417. aPropSet[cProp].guidPropertySet = DBPROPSET_NLCOMMAND;
  418. cProp++;
  419. }
  420. if ( 0 != _iResourceFactor )
  421. {
  422. // Set the query timeout in milliseconds
  423. Win4Assert( cProp < MAX_PROPS );
  424. aProp[cProp].dwPropertyID = DBPROP_COMMANDTIMEOUT;
  425. aProp[cProp].dwOptions = DBPROPOPTIONS_OPTIONAL;
  426. aProp[cProp].dwStatus = 0; // Ignored
  427. aProp[cProp].colid = dbcolNull;
  428. aProp[cProp].vValue.vt = VT_I4;
  429. aProp[cProp].vValue.lVal = _iResourceFactor;
  430. aPropSet[cProp].rgProperties = &aProp[cProp];
  431. aPropSet[cProp].cProperties = 1;
  432. aPropSet[cProp].guidPropertySet = guidRowsetProps;
  433. cProp++;
  434. }
  435. if ( cProp > 0 )
  436. {
  437. ICommandProperties * pCmdProp = 0;
  438. sc = xICommand->QueryInterface(IID_ICommandProperties,
  439. (void **)&pCmdProp);
  440. if (FAILED (sc) )
  441. {
  442. THROW( CException( QUERY_EXECUTE_FAILED ) );
  443. }
  444. sc = pCmdProp->SetProperties( cProp, aPropSet );
  445. pCmdProp->Release();
  446. if ( DB_S_ERRORSOCCURRED == sc ||
  447. DB_E_ERRORSOCCURRED == sc )
  448. {
  449. // Ignore an 'unsupported' error trying to set the GetHitCount
  450. // property.
  451. unsigned cErrors = 0;
  452. for (unsigned i = 0; i < cProp; i++)
  453. {
  454. if ( i == iHitCountProp &&
  455. aProp[i].dwStatus == DBPROPSTATUS_NOTSUPPORTED )
  456. continue;
  457. if (aProp[i].dwStatus != DBPROPSTATUS_OK)
  458. cErrors++;
  459. }
  460. if ( 0 == cErrors )
  461. sc = S_OK;
  462. }
  463. if ( FAILED(sc) || DB_S_ERRORSOCCURRED == sc )
  464. {
  465. THROW( CException( QUERY_EXECUTE_FAILED ) );
  466. }
  467. }
  468. //
  469. // Execute the query
  470. //
  471. sc = xICommand->Execute( 0, // No aggr
  472. IsSequential() ? IID_IRowset : IID_IRowsetExactScroll,
  473. 0, // disp params
  474. 0, // # rowsets returned
  475. (IUnknown **) &_pIRowset );
  476. if ( FAILED(sc) )
  477. {
  478. ERRORINFO ErrorInfo;
  479. XInterface<IErrorInfo> xErrorInfo;
  480. SCODE sc2 = GetOleDBErrorInfo(xICommand.GetPointer(),
  481. IID_ICommand,
  482. _lcid,
  483. eMostDetailedCIError,
  484. &ErrorInfo,
  485. (IErrorInfo **)xErrorInfo.GetQIPointer());
  486. // Post IErrorInfo only if we have a valid ptr to it
  487. if (SUCCEEDED(sc2) && 0 != xErrorInfo.GetPointer())
  488. {
  489. sc = ErrorInfo.hrError;
  490. THROW( CPostedOleDBException(sc, xErrorInfo.GetPointer()) );
  491. }
  492. else
  493. THROW( CException(sc) );
  494. }
  495. xICommand.Acquire()->Release();
  496. //
  497. // Create some of the restriction specific variables.
  498. //
  499. //
  500. // Get _pIRowsetQueryStatus interface
  501. //
  502. sc = _pIRowset->QueryInterface( IID_IRowsetQueryStatus,
  503. (void **) &_pIRowsetQueryStatus );
  504. if ( FAILED(sc) )
  505. {
  506. THROW( CException(sc) );
  507. }
  508. Win4Assert( 0 != _pIRowsetQueryStatus );
  509. }
  510. //+---------------------------------------------------------------------------
  511. //
  512. // Member: CixssoQuery::GetQueryStatus - private
  513. //
  514. // Synopsis: If a query is active, returns the query status
  515. //
  516. // Arguments: NONE
  517. //
  518. // Returns: DWORD - query status
  519. //
  520. // History: 15 Nov 1996 Alanw Created
  521. //
  522. //----------------------------------------------------------------------------
  523. DWORD CixssoQuery::GetQueryStatus( )
  524. {
  525. DWORD dwStatus = 0;
  526. SCODE sc;
  527. if ( ! _pIRowsetQueryStatus )
  528. THROW( CIxssoException(MSG_IXSSO_NO_ACTIVE_QUERY, 0) );
  529. sc = _pIRowsetQueryStatus->GetStatus( &dwStatus );
  530. if ( ! SUCCEEDED(sc) )
  531. THROW( CException( sc ) );
  532. return dwStatus;
  533. }
  534. //+---------------------------------------------------------------------------
  535. //
  536. // Member: CixssoQuery::IsAVirtualPath - private
  537. //
  538. // Synopsis: Determines if the path passed is a virtual or physical path.
  539. // If it's a virtual path, then / are changed to \.
  540. //
  541. // History: 96-Feb-14 DwightKr Created
  542. //
  543. //----------------------------------------------------------------------------
  544. BOOL CixssoQuery::IsAVirtualPath( WCHAR * wcsPath )
  545. {
  546. Win4Assert ( 0 != wcsPath );
  547. if ( 0 == wcsPath[0] )
  548. {
  549. return TRUE;
  550. }
  551. if ( (L':' == wcsPath[1]) || (L'\\' == wcsPath[0]) )
  552. {
  553. return FALSE;
  554. }
  555. else
  556. {
  557. //
  558. // Flip slashes to backslashes
  559. //
  560. for ( WCHAR *wcsLetter = wcsPath;
  561. *wcsLetter != 0;
  562. wcsLetter++
  563. )
  564. {
  565. if ( L'/' == *wcsLetter )
  566. {
  567. *wcsLetter = L'\\';
  568. }
  569. }
  570. }
  571. return TRUE;
  572. }