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.

682 lines
18 KiB

  1. //+---------------------------------------------------------------------------
  2. //
  3. // Microsoft Windows
  4. // Copyright (C) Microsoft Corporation, 2000.
  5. //
  6. // File: V R O O T S . C P P
  7. //
  8. // Contents: Implements the virtual root system for the HTTP server
  9. //
  10. // Notes:
  11. //
  12. // Author: danielwe 2000/11/6
  13. //
  14. //----------------------------------------------------------------------------
  15. #include "pch.h"
  16. #pragma hdrstop
  17. #include "httpd.h"
  18. #include "ncreg.h"
  19. // Used to split a URL and Path Translated apart for ISAPI/ASP scripts
  20. inline void SetPathInfo(PSTR *ppszPathInfo,PSTR pszInputURL,int iURLLen)
  21. {
  22. int iLen = strlen(pszInputURL+iURLLen) + 2;
  23. // If we've mapped the virtual root "/" to a script, need an extra "/" for the path
  24. // (normally we use the origial trailing "/", but in this case the "/" is the URL
  25. // BUGBUG: Probably should rewrite the Virtual roots parsing
  26. // mechanism so that it's cleaner.
  27. *ppszPathInfo = MySzAllocA((iURLLen == 1) ? iLen + 1 : iLen);
  28. if (! (*ppszPathInfo))
  29. goto done;
  30. if (iURLLen == 1)
  31. {
  32. (*ppszPathInfo)[0] = '/';
  33. memcpy( (*ppszPathInfo) +1, pszInputURL + iURLLen, iLen);
  34. }
  35. else
  36. memcpy(*ppszPathInfo, pszInputURL + iURLLen, iLen);
  37. done:
  38. // URL shouldn't contain path info, break it apart
  39. pszInputURL[iURLLen] = 0;
  40. }
  41. PVROOTINFO CVRoots::MatchVRoot(PCSTR pszInputURL, int iInputLen)
  42. {
  43. int i, iMatch;
  44. // If there was an error on setting up the vroots, m_pVRoots = NULL.
  45. if (!m_pVRoots)
  46. return NULL;
  47. for(i=0, iMatch=-1; i<m_nVRoots; i++)
  48. {
  49. int iLen = m_pVRoots[i].iURLLen;
  50. // If this root maps to physical path "\", special case.
  51. // In general we store pszURL without trailing "/", however we have
  52. // to store trailing "/" for root directory.
  53. if (m_pVRoots[i].bRootDir && iLen != 1)
  54. iLen--;
  55. if(iLen && iInputLen >= iLen)
  56. {
  57. if(0 == _memicmp(pszInputURL, m_pVRoots[i].pszURL, iLen))
  58. {
  59. // If it's not root dir, always matched. Otherwise it's possible
  60. // there wasn't a match. For root dirs, pszURL[iLen] is always "/"
  61. if (!m_pVRoots[i].bRootDir || m_pVRoots[i].iURLLen == 1 || pszInputURL[iLen] == '/' || pszInputURL[iLen] == '\0')
  62. {
  63. TraceTag(ttidWebServer, "URL %s matched VRoot %s (path %S, perm=%d, auth=%d)",
  64. pszInputURL, m_pVRoots[i].pszURL,
  65. m_pVRoots[i].wszPath,
  66. m_pVRoots[i].dwPermissions,
  67. m_pVRoots[i].AuthLevel);
  68. return &(m_pVRoots[i]);
  69. }
  70. }
  71. }
  72. }
  73. TraceTag(ttidWebServer, "URL %s did not matched any VRoot", pszInputURL);
  74. return NULL;
  75. }
  76. BOOL CVRoots::FillVRoot(PVROOTINFO pvr, LPWSTR wszURL, LPWSTR wszPath)
  77. {
  78. int err = 0; // err variable is used in non-Debug mode
  79. const char cszDLL[] = ".dll";
  80. const char cszASP[] = ".asp";
  81. CHAR pszURL[MAX_PATH+1];
  82. CHAR pszPath[MAX_PATH+1];
  83. // convert URL to MBCS
  84. int iLen = pvr->iURLLen = MyW2A(wszURL, pszURL, sizeof(pszURL));
  85. if(!iLen)
  86. { myleave(83); }
  87. pvr->iURLLen--; // -1 for null-term
  88. pvr->iPathLen = wcslen(wszPath);
  89. MyW2A(wszPath, pszPath, sizeof(pszPath));
  90. // check to see if Vroot ends in .dll or .asp, in this case we send
  91. // client not to the directory but to the script page.
  92. if (pvr->iPathLen >= sizeof(cszDLL) &&
  93. 0 == strcmpi(pszPath + pvr->iPathLen - sizeof(cszDLL) +1,cszDLL))
  94. {
  95. pvr->ScriptType = SCRIPT_TYPE_EXTENSION;
  96. }
  97. else if (pvr->iPathLen >= sizeof(cszASP) &&
  98. 0 == strcmpi(pszPath + pvr->iPathLen - sizeof(cszASP) +1,cszASP))
  99. {
  100. pvr->ScriptType = SCRIPT_TYPE_ASP;
  101. }
  102. else
  103. {
  104. pvr->ScriptType = SCRIPT_TYPE_NONE;
  105. }
  106. // If one of URL or path ends in a slash, the other must too.
  107. // If either the URL ends in a "/" or when the path ends in "\", we remove
  108. // the extra symbol. However, in the case where either URL or path is
  109. // root we don't do this.
  110. if (pvr->iURLLen != 1 && pszURL[pvr->iURLLen-1]=='/')
  111. {
  112. pszURL[pvr->iURLLen-1] = L'\0';
  113. pvr->iURLLen--;
  114. }
  115. else if (pvr->iURLLen == 1 && pszURL[0]=='/' && pvr->ScriptType == SCRIPT_TYPE_NONE)
  116. {
  117. // if it's the root URL, make sure correspinding path ends with "\"
  118. // (if it's a directory only, leave ASP + ISAPI's alone)
  119. if (wszPath[pvr->iPathLen-1] != L'\\')
  120. {
  121. wszPath[pvr->iPathLen] = L'\\';
  122. pvr->iPathLen++;
  123. wszPath[pvr->iPathLen] = L'\0';
  124. }
  125. }
  126. // If Path ends in "\" (and it's not the root path or root virtual root)
  127. // remove the "\"
  128. if (pvr->iURLLen != 1 && pvr->iPathLen != 1 && wszPath[pvr->iPathLen-1]==L'\\')
  129. {
  130. wszPath[pvr->iPathLen-1] = L'\0';
  131. pvr->iPathLen--;
  132. }
  133. else if (pvr->iPathLen == 1 && wszPath[0]==L'\\')
  134. {
  135. // Trailing "/" must match "\". However, we need a slight HACK to make this work
  136. if (pszURL[pvr->iURLLen-1] != '/')
  137. {
  138. pszURL[pvr->iURLLen] = '/';
  139. pvr->iURLLen++;
  140. pszURL[pvr->iURLLen] = '\0';
  141. }
  142. pvr->bRootDir = TRUE;
  143. }
  144. pvr->pszURL = MySzDupA(pszURL);
  145. pvr->wszPath = MySzDupW(wszPath);
  146. // Fill in defaults for these
  147. pvr->wszUserList = NULL;
  148. pvr->dwPermissions = HTTP_DEFAULTP_PERMISSIONS;
  149. pvr->AuthLevel = AUTH_PUBLIC;
  150. TraceTag(ttidWebServer, "VROOT: (%s)=>(%s) perm=%d auth=%d ScriptType=%d",
  151. pvr->pszURL, pvr->wszPath, pvr->dwPermissions,
  152. pvr->AuthLevel,pvr->ScriptType);
  153. done:
  154. if(err)
  155. {
  156. return FALSE;
  157. }
  158. return TRUE;
  159. }
  160. VOID CVRoots::Sort()
  161. {
  162. BOOL fChange;
  163. int i=0;
  164. // We now want to sort the VRoots in descending order of URL-length so
  165. // that when we match we'll find the longest match first!!
  166. // Using a slow bubble-sort :-(
  167. do {
  168. fChange = FALSE;
  169. for(i=0; i<m_nVRoots-1; i++)
  170. {
  171. if(m_pVRoots[i].iURLLen < m_pVRoots[i+1].iURLLen)
  172. {
  173. // swap the 2 vroots
  174. VROOTINFO vtemp = m_pVRoots[i+1];
  175. m_pVRoots[i+1] = m_pVRoots[i];
  176. m_pVRoots[i] = vtemp;
  177. fChange = TRUE;
  178. }
  179. }
  180. } while(fChange);
  181. }
  182. static const WCHAR c_szPrefix[] = L"\\\\?\\";
  183. static const int c_cchPrefix = celems(c_szPrefix);
  184. BOOL CVRoots::Init()
  185. {
  186. int err = 0; // err variable is used in non-Debug mode
  187. int i=0;
  188. InitializeCriticalSection(&m_csVroot);
  189. // Registry doesnt allow keynames longer than MAX_PATH so we won't map URL prefixes longer than MAX_PATH
  190. WCHAR wszURL[MAX_PATH+1];
  191. WCHAR wszPath[MAX_PATH+1];
  192. WCHAR wszPathReal[MAX_PATH + 1] = {0};
  193. wszURL[0]=wszPath[0]=0;
  194. // open the VRoots key
  195. CReg topreg(HKEY_LOCAL_MACHINE, RK_HTTPDVROOTS);
  196. // allocate space for as many VRoots as we have subkeys
  197. m_nVRoots = topreg.NumSubkeys();
  198. if(!m_nVRoots)
  199. myleave(80);
  200. // Zero the memory so we know what to deallocate and what not to.
  201. if(!(m_pVRoots = MyRgAllocZ(VROOTINFO, m_nVRoots)))
  202. myleave(81);
  203. // enumerate all subkeys. Their names are URLs, their default value is the corresponding path
  204. // Note: EnumKey takes sizes in chars, not bytes!
  205. for(i=0; i<m_nVRoots && topreg.EnumKey(wszURL, CCHSIZEOF(wszURL)); i++)
  206. {
  207. CReg subreg(topreg, wszURL);
  208. // get the unnamed value. Again size is in chars, not bytes.
  209. if(!subreg.ValueSZ(NULL, wszPath, CCHSIZEOF(wszPath)))
  210. {
  211. // iURLLen and iPathLen set to 0 already, so no case of corruption in MatchVRoot
  212. subreg.Reset();
  213. continue;
  214. }
  215. else
  216. {
  217. // Prepend the \\?\ prefix
  218. //
  219. lstrcpy(wszPathReal, c_szPrefix);
  220. lstrcat(wszPathReal, wszPath);
  221. if (!FillVRoot(&m_pVRoots[i], wszURL, wszPathReal))
  222. myleave(121);
  223. m_pVRoots[i].wszUserList = MySzDupW( subreg.ValueSZ(RV_USERLIST));
  224. // default permissions is Read & Execute
  225. m_pVRoots[i].dwPermissions = subreg.ValueDW(RV_PERM, HTTP_DEFAULTP_PERMISSIONS);
  226. // default authentication is public
  227. m_pVRoots[i].AuthLevel = (AUTHLEVEL)subreg.ValueDW(RV_AUTH, (DWORD)AUTH_PUBLIC);
  228. // we don't fail if we can't load an extension map
  229. LoadExtensionMap (&m_pVRoots[i], subreg);
  230. }
  231. subreg.Reset();
  232. }
  233. Sort();
  234. done:
  235. if(err)
  236. {
  237. TraceTag(ttidWebServer, "CVRoots::ctor FAILED due to err=%d GLE=%d "
  238. "(num=%d i=%d pVRoots=0x%08x url=%s path=%s)",
  239. err, GetLastError(), m_nVRoots, i, m_pVRoots, wszURL, wszPath);
  240. return FALSE;
  241. }
  242. return TRUE;
  243. }
  244. void CVRoots::Cleanup()
  245. {
  246. if(!m_pVRoots)
  247. return;
  248. for(int i=0; i<m_nVRoots; i++)
  249. {
  250. MyFree(m_pVRoots[i].pszURL);
  251. MyFree(m_pVRoots[i].wszPath);
  252. MyFree(m_pVRoots[i].wszUserList);
  253. FreeExtensionMap (&m_pVRoots[i]);
  254. }
  255. MyFree(m_pVRoots);
  256. DeleteCriticalSection(&m_csVroot);
  257. }
  258. BOOL CVRoots::AddVRoot(LPWSTR szUrl, LPWSTR szPath)
  259. {
  260. PVROOTINFO pvrNew;
  261. int err = 0;
  262. LPSTR szaUrl = NULL;
  263. int iInputLen;
  264. szaUrl = SzFromWsz(szUrl);
  265. if (!szaUrl)
  266. {
  267. // Can't use myleave since we don't have the critsec here
  268. //
  269. err = 400;
  270. goto err;
  271. }
  272. iInputLen = strlen(szaUrl);
  273. EnterCriticalSection(&m_csVroot);
  274. pvrNew = MatchVRoot(szaUrl, iInputLen);
  275. if(pvrNew)
  276. {
  277. TraceError("CVRoots::AddVRoot - already present", E_FAIL);
  278. myleave(10);
  279. }
  280. m_pVRoots = MyRgReAlloc(VROOTINFO, m_pVRoots, m_nVRoots, m_nVRoots + 1);
  281. if(!m_nVRoots)
  282. myleave(100);
  283. pvrNew = &m_pVRoots[m_nVRoots];
  284. if (!FillVRoot(pvrNew, szUrl, szPath))
  285. {
  286. myleave(101);
  287. }
  288. m_nVRoots++;
  289. HKEY hkeyVroot;
  290. HKEY hkeyNew;
  291. HRESULT hr;
  292. hr = HrRegOpenKeyEx(HKEY_LOCAL_MACHINE, RK_HTTPDVROOTS, KEY_ALL_ACCESS,
  293. &hkeyVroot);
  294. if (SUCCEEDED(hr))
  295. {
  296. hr = HrRegCreateKeyEx(hkeyVroot, szUrl, 0, KEY_ALL_ACCESS, NULL,
  297. &hkeyNew, NULL);
  298. if (SUCCEEDED(hr))
  299. {
  300. // Pass NULL to set default value
  301. //
  302. hr = HrRegSetSz(hkeyNew, NULL, szPath);
  303. RegCloseKey(hkeyNew);
  304. }
  305. RegCloseKey(hkeyVroot);
  306. }
  307. if (FAILED(hr))
  308. {
  309. TraceError("CVRoots::AddVRoot", hr);
  310. myleave(111);
  311. }
  312. else
  313. {
  314. Sort();
  315. }
  316. done:
  317. delete [] szaUrl;
  318. LeaveCriticalSection(&m_csVroot);
  319. err:
  320. if(err)
  321. {
  322. return FALSE;
  323. }
  324. return TRUE;
  325. }
  326. BOOL CVRoots::RemoveVRoot(LPWSTR szwUrl)
  327. {
  328. int ivr;
  329. LPSTR szUrl = NULL;
  330. PVROOTINFO pvr;
  331. int iInputLen;
  332. int err = 0;
  333. BOOL fFound = FALSE;
  334. szUrl = SzFromWsz(szwUrl);
  335. if (!szUrl)
  336. {
  337. myleave(100);
  338. }
  339. iInputLen = strlen(szUrl);
  340. EnterCriticalSection(&m_csVroot);
  341. pvr = MatchVRoot(szUrl, iInputLen);
  342. if(!pvr)
  343. {
  344. myleave(140);
  345. }
  346. for (ivr = 0; ivr < m_nVRoots; ivr++)
  347. {
  348. if (&m_pVRoots[ivr] == pvr)
  349. {
  350. // Found the one to remove. So now let's shift the rest down by
  351. // one
  352. //
  353. MoveMemory(&m_pVRoots[ivr], &m_pVRoots[ivr + 1],
  354. sizeof(VROOTINFO) * (m_nVRoots - ivr - 1));
  355. m_nVRoots--;
  356. fFound = TRUE;
  357. break;
  358. }
  359. }
  360. AssertSz(fFound, "How come it was there a minute ago??");
  361. HKEY hkeyVroot;
  362. HKEY hkeyNew;
  363. HRESULT hr;
  364. hr = HrRegOpenKeyEx(HKEY_LOCAL_MACHINE, RK_HTTPDVROOTS, KEY_ALL_ACCESS,
  365. &hkeyVroot);
  366. if (SUCCEEDED(hr))
  367. {
  368. hr = HrRegDeleteKey(hkeyVroot, szwUrl);
  369. RegCloseKey(hkeyVroot);
  370. }
  371. if (FAILED(hr))
  372. {
  373. TraceError("CVRoots::RemoveVRoot", hr);
  374. myleave(111);
  375. }
  376. // No need to re-sort since we moved everything down by one
  377. done:
  378. LeaveCriticalSection(&m_csVroot);
  379. delete [] szUrl;
  380. if(err)
  381. {
  382. return FALSE;
  383. }
  384. return TRUE;
  385. }
  386. PWSTR CVRoots::URLAtoPathW(PSTR pszInputURL, PDWORD pdwPerm /*=0*/,
  387. AUTHLEVEL* pAuthLevel /* =0 */,
  388. SCRIPT_TYPE *pScriptType /*=0 */,
  389. PSTR *ppszPathInfo /* =0 */,
  390. WCHAR **ppwszUserList /*=0 */)
  391. {
  392. PSTR pszEndOfURL;
  393. WCHAR *wszTemp = NULL;
  394. int iInputLen = strlen(pszInputURL);
  395. EnterCriticalSection(&m_csVroot);
  396. PVROOTINFO pVRoot = MatchVRoot(pszInputURL, iInputLen);
  397. if(!pVRoot)
  398. {
  399. LeaveCriticalSection(&m_csVroot);
  400. return NULL;
  401. }
  402. // Do a lookup to see if the current URL contains and extension
  403. // that is in the extension map for the VROOT. If so, this call
  404. // will truncate the URL after the extension. Since the URL may
  405. // have changed, the length is re-obtained on a successfull call.
  406. if (MapExtToPath (pszInputURL, &pszEndOfURL))
  407. {
  408. if (ppszPathInfo && *ppszPathInfo == NULL)
  409. *ppszPathInfo = MySzDupA (pszInputURL);
  410. if (*pszEndOfURL != '\0')
  411. {
  412. *pszEndOfURL = 0;
  413. iInputLen = strlen(pszInputURL);
  414. }
  415. }
  416. // in computing the buffersize here we are assuming that an MBCS string of length N
  417. // cannot produce a unicode string of length greater than N
  418. int iOutLen = 1 + pVRoot->iPathLen + (iInputLen - pVRoot->iURLLen);
  419. PWSTR wszOutPath = MyRgAllocNZ(WCHAR, iOutLen);
  420. if (!wszOutPath)
  421. {
  422. LeaveCriticalSection(&m_csVroot);
  423. return NULL;
  424. }
  425. // assemble the path. First, the mapped base path
  426. memcpy(wszOutPath, pVRoot->wszPath, sizeof(WCHAR)*pVRoot->iPathLen);
  427. if(pdwPerm)
  428. *pdwPerm = pVRoot->dwPermissions;
  429. if(pAuthLevel)
  430. *pAuthLevel = pVRoot->AuthLevel;
  431. if (pScriptType)
  432. *pScriptType = pVRoot->ScriptType;
  433. if (ppwszUserList)
  434. *ppwszUserList = pVRoot->wszUserList;
  435. // If the vroot specifies an ISAPI dll or ASP page don't copy path info over.
  436. if (pVRoot->ScriptType != SCRIPT_TYPE_NONE)
  437. {
  438. if ( ppszPathInfo && pszInputURL[pVRoot->iURLLen] != 0)
  439. {
  440. SetPathInfo(ppszPathInfo,pszInputURL,pVRoot->iURLLen);
  441. }
  442. wszOutPath[pVRoot->iPathLen] = L'\0';
  443. LeaveCriticalSection(&m_csVroot);
  444. return wszOutPath;
  445. }
  446. // next the remainder of the URL, converted to wide
  447. if (iOutLen-pVRoot->iPathLen == 0)
  448. {
  449. wszOutPath[pVRoot->iPathLen] = L'\0';
  450. }
  451. else
  452. {
  453. int iRet = MyA2W(pszInputURL+pVRoot->iURLLen, wszOutPath+pVRoot->iPathLen, iOutLen-pVRoot->iPathLen);
  454. DEBUGCHK(iRet);
  455. }
  456. LeaveCriticalSection(&m_csVroot);
  457. // Flip forward slashes around to backslashes
  458. LPWSTR pchFlip = wszOutPath;
  459. while (*pchFlip)
  460. {
  461. if (*pchFlip == '/')
  462. {
  463. *pchFlip = '\\';
  464. }
  465. pchFlip++;
  466. }
  467. return wszOutPath;
  468. }
  469. BOOL CVRoots::LoadExtensionMap(PVROOTINFO pvr, HKEY rootreg)
  470. {
  471. BOOL bWildCard;
  472. WCHAR wszExt[MAX_PATH+1];
  473. WCHAR wszPath[MAX_PATH+1];
  474. CReg mapreg(rootreg, RV_EXTENSIONMAP);
  475. if (!mapreg.IsOK())
  476. {
  477. pvr->nExtensions = 0;
  478. return FALSE;
  479. }
  480. if (bWildCard = mapreg.ValueSZ(NULL, wszPath, CCHSIZEOF(wszPath)))
  481. pvr->nExtensions = 1;
  482. else
  483. pvr->nExtensions = mapreg.NumValues();
  484. if ((pvr->pExtMap = MyRgAllocZ(EXTMAP, pvr->nExtensions)) == NULL)
  485. return FALSE;
  486. if (bWildCard)
  487. {
  488. pvr->pExtMap[0].wszExt = MySzDupW(L"*");
  489. pvr->pExtMap[0].wszPath = MySzDupW(wszPath);
  490. return TRUE;
  491. }
  492. for (int i = 0; i < pvr->nExtensions; i++)
  493. {
  494. if (mapreg.EnumValue(wszExt, CCHSIZEOF(wszExt),
  495. wszPath, CCHSIZEOF(wszPath)))
  496. {
  497. if (wszExt[0] != 0 && wszPath[0] != 0)
  498. {
  499. pvr->pExtMap[i].wszExt = MySzDupW(wszExt);
  500. pvr->pExtMap[i].wszPath = MySzDupW(wszPath);
  501. }
  502. else
  503. {
  504. pvr->pExtMap[i].wszExt = MySzDupW(L"");
  505. pvr->pExtMap[i].wszPath = MySzDupW(L"");
  506. }
  507. }
  508. }
  509. return TRUE;
  510. }
  511. void CVRoots::FreeExtensionMap (PVROOTINFO pvr)
  512. {
  513. if (pvr->pExtMap == NULL) return;
  514. for (int i=0; i < pvr->nExtensions; i++)
  515. {
  516. MyFree(pvr->pExtMap[i].wszExt);
  517. MyFree(pvr->pExtMap[i].wszPath);
  518. }
  519. MyFree(pvr->pExtMap);
  520. pvr->pExtMap = NULL;
  521. }
  522. PWSTR CVRoots::MapExtToPath (PSTR pszInputURL, PSTR *ppszEndOfURL)
  523. {
  524. PSTR pszTemp, pszStart, pszEnd;
  525. PWSTR wszPath = NULL;
  526. WCHAR wszExt[MAX_PATH+1];
  527. PVROOTINFO pvr;
  528. int iInputLen = strlen(pszInputURL);
  529. if (!FindExtInURL (pszInputURL, &pszStart, &pszEnd))
  530. return NULL;
  531. MyA2W (pszStart, wszExt, (int)((INT_PTR)(pszEnd - pszStart)));
  532. wszExt [pszEnd - pszStart] = L'\0';
  533. EnterCriticalSection(&m_csVroot);
  534. if (!(pvr = MatchVRoot(pszInputURL, iInputLen)))
  535. {
  536. LeaveCriticalSection(&m_csVroot);
  537. return NULL;
  538. }
  539. for (int i = 0; i < pvr->nExtensions; i++)
  540. {
  541. if ((i == 0 && pvr->pExtMap[i].wszExt[0] == L'*') ||
  542. 0 == _wcsicmp (pvr->pExtMap[i].wszExt, wszExt))
  543. {
  544. wszPath = pvr->pExtMap[i].wszPath;
  545. if (ppszEndOfURL != NULL)
  546. *ppszEndOfURL = pszEnd;
  547. break;
  548. }
  549. }
  550. LeaveCriticalSection(&m_csVroot);
  551. return wszPath;
  552. }
  553. BOOL CVRoots::FindExtInURL (PSTR pszInputURL, PSTR *ppszStart, PSTR *ppszEnd)
  554. {
  555. *ppszStart = strrchr (pszInputURL, '.');
  556. if ((*ppszEnd = *ppszStart) == NULL)
  557. return FALSE;
  558. while (**ppszEnd != '/' && **ppszEnd != '\0')
  559. *ppszEnd = *ppszEnd + 1;
  560. return TRUE;
  561. }