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.

826 lines
24 KiB

  1. //+-------------------------------------------------------------------------
  2. //
  3. // Microsoft Windows
  4. //
  5. // Copyright (C) Microsoft Corporation, 2000
  6. //
  7. // File: nopin.cpp
  8. //
  9. //--------------------------------------------------------------------------
  10. #include "pch.h"
  11. #pragma hdrstop
  12. #include "eventlog.h"
  13. #include "nopin.h"
  14. #include "strings.h"
  15. #include "msg.h"
  16. //--------------------------------------------------------------------------
  17. // class CNoPinList
  18. //--------------------------------------------------------------------------
  19. CNoPinList::CNoPinList(
  20. void
  21. ) : m_pRoot(NULL)
  22. {
  23. }
  24. CNoPinList::~CNoPinList(
  25. void
  26. )
  27. {
  28. delete m_pRoot;
  29. }
  30. //
  31. // Searches the tree for a COMPLETE path that is a subpath of
  32. // pszPath. If found, pszPath specifies a file or folder
  33. // that cannot be pinned.
  34. //
  35. // Returns:
  36. // S_OK - Pinning is allowed.
  37. // S_FALSE - Pinning is NOT allowed.
  38. // NOPIN_E_BADPATH - Path is not a valid UNC.
  39. //
  40. HRESULT
  41. CNoPinList::IsPinAllowed(
  42. LPCTSTR pszPath
  43. )
  44. {
  45. TraceEnter(TRACE_UTIL, "CNoPinList::IsPinAllowed");
  46. TraceAssert(NULL != pszPath);
  47. TraceAssert(::PathIsUNC(pszPath));
  48. HRESULT hr = _Initialize();
  49. if (SUCCEEDED(hr))
  50. {
  51. hr = S_OK;
  52. //
  53. // A quick optimization is to see if the tree is empty.
  54. // If it is, any file/folder may be pinned. This helps
  55. // performance when no pinning restriction is in place.
  56. //
  57. TraceAssert(NULL != m_pRoot);
  58. if (m_pRoot->HasChildren())
  59. {
  60. if (::PathIsUNC(pszPath))
  61. {
  62. //
  63. // SubPathExists modifies the path. Need to make a local copy.
  64. //
  65. TCHAR szPath[MAX_PATH];
  66. hr = StringCchCopy(szPath, ARRAYSIZE(szPath), pszPath);
  67. if (SUCCEEDED(hr))
  68. {
  69. hr = m_pRoot->SubPathExists(szPath);
  70. if (S_FALSE == hr)
  71. {
  72. //
  73. // Absence from the tree means pinning is allowed.
  74. //
  75. hr = S_OK;
  76. }
  77. else if (S_OK == hr)
  78. {
  79. //
  80. // Presence in the tree means pinning is not allowed.
  81. //
  82. Trace((TEXT("Policy disallows pinning \"%s\""), pszPath));
  83. hr = S_FALSE;
  84. }
  85. }
  86. else if (STRSAFE_E_INSUFFICIENT_BUFFER == hr)
  87. {
  88. hr = NOPIN_E_BADPATH;
  89. }
  90. }
  91. else
  92. {
  93. hr = NOPIN_E_BADPATH;
  94. }
  95. }
  96. }
  97. TraceAssert(S_OK == hr ||
  98. S_FALSE == hr ||
  99. NOPIN_E_BADPATH == hr);
  100. TraceLeaveResult(hr);
  101. }
  102. //
  103. // Quick check to see if ANY pin might be disallowed.
  104. // Returns:
  105. // S_OK - Tree has content.
  106. // S_FALSE - Tree is empty.
  107. //
  108. HRESULT
  109. CNoPinList::IsAnyPinDisallowed(
  110. void
  111. )
  112. {
  113. HRESULT hr = _Initialize();
  114. if (SUCCEEDED(hr))
  115. {
  116. TraceAssert(NULL != m_pRoot);
  117. hr = m_pRoot->HasChildren() ? S_OK : S_FALSE;
  118. }
  119. return hr;
  120. }
  121. //
  122. // Initializes the no-pin list by reading path strings from the
  123. // registry. The paths are stored in both HKLM and HKCU under
  124. // the following key:
  125. //
  126. // Software\Policies\Microsoft\Windows\NetCache\NoMakeAvailableOfflineList
  127. //
  128. // Path strings may contain environment variables.
  129. // Upon return the object contains a tree representing the union of all
  130. // files and folders listed in both registry keys.
  131. //
  132. // Errors in reading the registry result only in paths not being added
  133. // to the tree. No error is returned as a result of registry errors.
  134. // If an invalid UNC path is found in the registry, an even log entry
  135. // is recorded.
  136. //
  137. // Returns:
  138. // S_OK - List successfully loaded.
  139. // S_FALSE - List already initialized.
  140. // E_OUTOFMEMORY - Insufficient memory.
  141. // Other errors are possible.
  142. //
  143. HRESULT
  144. CNoPinList::_Initialize(
  145. void
  146. )
  147. {
  148. TraceEnter(TRACE_UTIL, "CNoPinList::_Initialize");
  149. HRESULT hr = S_OK;
  150. if (NULL != m_pRoot)
  151. {
  152. //
  153. // List is already initialized.
  154. //
  155. hr = S_FALSE;
  156. }
  157. else
  158. {
  159. m_pRoot = new CNode;
  160. if (NULL == m_pRoot)
  161. {
  162. hr = E_OUTOFMEMORY;
  163. }
  164. else
  165. {
  166. const HKEY rghkeyRoot[] = { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE };
  167. TCHAR szKey[MAX_PATH];
  168. PathCombine(szKey, REGSTR_KEY_OFFLINEFILESPOLICY, REGSTR_SUBKEY_NOMAKEAVAILABLEOFFLINELIST);
  169. for (int i = 0; SUCCEEDED(hr) && i < ARRAYSIZE(rghkeyRoot); i++)
  170. {
  171. HKEY hkey;
  172. LONG lResult = ::RegOpenKeyEx(rghkeyRoot[i],
  173. szKey,
  174. 0,
  175. KEY_QUERY_VALUE,
  176. &hkey);
  177. if (ERROR_SUCCESS == lResult)
  178. {
  179. TCHAR szName[MAX_PATH];
  180. DWORD dwIndex = 0;
  181. DWORD cchName = ARRAYSIZE(szName);
  182. //
  183. // Enumerate the paths listed in the registry.
  184. //
  185. while (SUCCEEDED(hr) &&
  186. ERROR_SUCCESS == ::RegEnumValue(hkey,
  187. dwIndex,
  188. szName,
  189. &cchName,
  190. NULL,
  191. NULL,
  192. NULL,
  193. NULL))
  194. {
  195. //
  196. // Install the path string from the registry into the
  197. // tree. This function will expand any embedded environment strings
  198. // as well as convert mapped drive specs to remote UNC paths.
  199. //
  200. hr = _InitPathFromRegistry(szName);
  201. if (NOPIN_E_BADPATH == hr)
  202. {
  203. //
  204. // This is a special error. It means someone has
  205. // put bad data into the registry. "Bad" meaning
  206. // that the path is not or does not expand to a valid UNC
  207. // path string.
  208. // Write an event log entry to tell the admin. The
  209. // entry is generated at event logging level 1. I don't want
  210. // it filling up an event log under normal conditions but
  211. // I want an admin to figure it out in case their no-pin
  212. // policy appears to be not working.
  213. //
  214. // The MSG template is this (english):
  215. //
  216. // "The registry value '%1' in key '%2\%3' is not, or does not
  217. // expand to, a valid UNC path."
  218. //
  219. // We handle the error here because this is where we still have the
  220. // value read from the registry. We include that in the event
  221. // log entry so the admin can easily find it.
  222. //
  223. CscuiEventLog log;
  224. log.Push(szName);
  225. if (HKEY_LOCAL_MACHINE == rghkeyRoot[i])
  226. {
  227. log.Push(TEXT("HKEY_LOCAL_MACHINE"));
  228. }
  229. else
  230. {
  231. log.Push(TEXT("HKEY_CURRENT_USER"));
  232. }
  233. log.Push(szKey);
  234. log.ReportEvent(EVENTLOG_WARNING_TYPE, MSG_W_INVALID_UNCPATH_INREG, 1);
  235. //
  236. // We do not abort processing because of a bad reg value.
  237. //
  238. hr = S_OK;
  239. }
  240. cchName = ARRAYSIZE(szName);
  241. dwIndex++;
  242. }
  243. ::RegCloseKey(hkey);
  244. hkey = NULL;
  245. }
  246. }
  247. }
  248. }
  249. TraceLeaveResult(hr);
  250. }
  251. //
  252. // Given a path string read from the registry, this function expands
  253. // any embedded environment strings, converts any mapped drive letters
  254. // to their corresponding remote UNC paths and installs the resulting
  255. // path string into the tree.
  256. //
  257. HRESULT
  258. CNoPinList::_InitPathFromRegistry(
  259. LPCTSTR pszPath
  260. )
  261. {
  262. TraceEnter(TRACE_UTIL, "CNoPinList::_InitPathFromRegistry");
  263. TraceAssert(NULL != pszPath);
  264. HRESULT hr = S_OK;
  265. TCHAR szNameExp[MAX_PATH]; // Expanded name string buffer.
  266. //
  267. // Expand any embedded environment strings.
  268. //
  269. if (0 == ::ExpandEnvironmentStrings(pszPath, szNameExp, ARRAYSIZE(szNameExp)))
  270. {
  271. const DWORD dwErr = GetLastError();
  272. hr = HRESULT_FROM_WIN32(dwErr);
  273. Trace((TEXT("Error %d expanding \"%s\""), dwErr, pszPath));
  274. }
  275. if (SUCCEEDED(hr))
  276. {
  277. LPCTSTR pszUncPath = NULL;
  278. LPTSTR pszRemotePath = NULL; // Created by GetRemotePath if necessary.
  279. //
  280. // Convert a common typing mistake.
  281. // Remember, these are reg entries. They could contain most anything.
  282. //
  283. for (LPTSTR s = szNameExp; *s; s++)
  284. {
  285. if (TEXT('/') == *s)
  286. {
  287. *s = TEXT('\\');
  288. }
  289. }
  290. if (::PathIsUNC(szNameExp))
  291. {
  292. //
  293. // Path is a UNC path. We're golden.
  294. //
  295. pszUncPath = szNameExp;
  296. }
  297. else
  298. {
  299. //
  300. // Path is probably a mapped drive.
  301. // Get its remote UNC path. This API returns S_FALSE
  302. // if the remote drive is not connected or if it's a local drive.
  303. //
  304. hr = ::GetRemotePath(szNameExp, &pszRemotePath);
  305. if (SUCCEEDED(hr))
  306. {
  307. if (S_OK == hr)
  308. {
  309. pszUncPath = pszRemotePath;
  310. }
  311. else if (S_FALSE == hr)
  312. {
  313. //
  314. // Path was either to a local drive or to a net drive that
  315. // isn't connected. Either way it's an invalid drive that
  316. // won't be considered in the no-pin logic. Use the expanded
  317. // value from the registry and pass that through to AddPath()
  318. // where it will be rejected as an invalid UNC path.
  319. //
  320. TraceAssert(NULL == pszRemotePath);
  321. pszUncPath = szNameExp;
  322. hr = S_OK;
  323. }
  324. }
  325. }
  326. if (SUCCEEDED(hr))
  327. {
  328. TraceAssert(NULL != pszUncPath);
  329. TraceAssert(pszUncPath == szNameExp || pszUncPath == pszRemotePath);
  330. //
  331. // Insert the UNC path into the tree.
  332. // At this point, a path may or may not be UNC. _AddPath()
  333. // will verify this.
  334. //
  335. hr = _AddPath(pszUncPath);
  336. }
  337. if (NULL != pszRemotePath)
  338. {
  339. ::LocalFree(pszRemotePath);
  340. }
  341. }
  342. TraceLeaveResult(hr);
  343. }
  344. //
  345. // Adds a path to the tree. If this is a sub-path of an existing
  346. // path in the tree, the remainder of the existing path is removed
  347. // from the tree.
  348. //
  349. // Returns:
  350. // S_OK - Path successfully added.
  351. // E_OUTOFMEMORY - Insufficient memory.
  352. // NOPIN_E_BADPATH - Invalid path string. Not a UNC.
  353. //
  354. HRESULT
  355. CNoPinList::_AddPath(
  356. LPCTSTR pszPath
  357. )
  358. {
  359. TraceAssert(NULL != pszPath);
  360. HRESULT hr = NOPIN_E_BADPATH;
  361. if (::PathIsUNC(pszPath))
  362. {
  363. //
  364. // AddPath modifies the path. Need to make a local copy.
  365. //
  366. TraceAssert(NULL != m_pRoot);
  367. TCHAR szPath[MAX_PATH];
  368. hr = StringCchCopy(szPath, ARRAYSIZE(szPath), pszPath);
  369. if (SUCCEEDED(hr))
  370. {
  371. hr = m_pRoot->AddPath(szPath);
  372. }
  373. else if (STRSAFE_E_INSUFFICIENT_BUFFER == hr)
  374. {
  375. hr = NOPIN_E_BADPATH;
  376. }
  377. }
  378. TraceAssert(S_OK == hr ||
  379. E_OUTOFMEMORY == hr ||
  380. NOPIN_E_BADPATH == hr);
  381. return hr;
  382. }
  383. //--------------------------------------------------------------------------
  384. // class CNoPinList::CNode
  385. //--------------------------------------------------------------------------
  386. CNoPinList::CNode::~CNode(
  387. void
  388. )
  389. {
  390. if (NULL != m_pszName)
  391. {
  392. ::LocalFree(m_pszName);
  393. }
  394. delete m_pChildren;
  395. delete m_pNext;
  396. }
  397. //
  398. // Initializes a node's name value.
  399. //
  400. HRESULT
  401. CNoPinList::CNode::Initialize(
  402. LPCTSTR pszName
  403. )
  404. {
  405. TraceAssert(NULL != pszName);
  406. TraceAssert(NULL == m_pszName);
  407. HRESULT hr = E_OUTOFMEMORY;
  408. if (LocalAllocString(&m_pszName, pszName))
  409. {
  410. hr = S_OK;
  411. }
  412. return hr;
  413. }
  414. //
  415. // Add a child, keeping the children in alphabetical
  416. // order by name. We trade a little time during creation
  417. // for the speed benefits during lookup.
  418. //
  419. void
  420. CNoPinList::CNode::_AddChild(
  421. CNode *pChild
  422. )
  423. {
  424. TraceAssert(NULL != pChild);
  425. CNode **ppNode = &m_pChildren;
  426. while(NULL != *ppNode)
  427. {
  428. CNode *pNode = *ppNode;
  429. //
  430. // Find the alphabetical insertion point.
  431. //
  432. TraceAssert(NULL != pNode->m_pszName);
  433. TraceAssert(NULL != pChild->m_pszName);
  434. int diff = ::lstrcmpi(pChild->m_pszName, pNode->m_pszName);
  435. if (0 == diff)
  436. {
  437. //
  438. // Child already exists. Don't allow duplicates.
  439. //
  440. return;
  441. }
  442. if (diff < 0)
  443. {
  444. //
  445. // The new child is alphabetically "greater" than the currently
  446. // visited node.
  447. // Exit the loop with ppNode pointing to the pointer variable
  448. // where we'll put the address of pChild.
  449. //
  450. break;
  451. }
  452. else
  453. {
  454. //
  455. // Advance to the next node in the list.
  456. //
  457. ppNode = &pNode->m_pNext;
  458. }
  459. }
  460. //
  461. // Insert the child.
  462. //
  463. pChild->m_pNext = *ppNode;
  464. *ppNode = pChild;
  465. }
  466. //
  467. // Locates a child node in a node's list of children.
  468. // Comparison is by node name.
  469. // Returns the address of the node if found. NULL otherwise.
  470. //
  471. CNoPinList::CNode *
  472. CNoPinList::CNode::_FindChild(
  473. LPCTSTR pszName
  474. ) const
  475. {
  476. TraceAssert(NULL != pszName);
  477. CNode *pChild = NULL;
  478. for (CNode *pNode = m_pChildren; pNode; pNode = pNode->m_pNext)
  479. {
  480. //
  481. // The list is sorted alphabetically.
  482. //
  483. int diff = ::lstrcmpi(pszName, pNode->m_pszName);
  484. if (diff <= 0)
  485. {
  486. //
  487. // Either we found a match or we've passed all possible
  488. // matches.
  489. //
  490. if (0 == diff)
  491. {
  492. //
  493. // Exact match.
  494. //
  495. pChild = pNode;
  496. }
  497. break;
  498. }
  499. }
  500. return pChild;
  501. }
  502. //
  503. // Given "\\brianau1\public\bin"
  504. // Returns address of "brianau1\public\bin", with *pcchComponent == 8.
  505. //
  506. // Given "public\bin"
  507. // Returns address of "bin", with *pcchComponent == 3.
  508. //
  509. LPCTSTR
  510. CNoPinList::CNode::_FindNextPathComponent( // [static]
  511. LPCTSTR pszPath,
  512. int *pcchComponent // [optional] Can be NULL.
  513. )
  514. {
  515. TraceAssert(NULL != pszPath);
  516. LPCTSTR pszBegin = pszPath;
  517. const TCHAR CH_BS = TEXT('\\');
  518. //
  519. // Skip any leading backslashes.
  520. //
  521. while(*pszBegin && CH_BS == *pszBegin)
  522. ++pszBegin;
  523. //
  524. // Find the end of the path component.
  525. //
  526. LPCTSTR pszEnd = pszBegin;
  527. while(*pszEnd && CH_BS != *pszEnd)
  528. ++pszEnd;
  529. if (NULL != pcchComponent)
  530. {
  531. *pcchComponent = int(pszEnd - pszBegin);
  532. TraceAssert(0 <= *pcchComponent);
  533. }
  534. //
  535. // Validate the final position of the begin and end ptrs.
  536. //
  537. TraceAssert(NULL != pszBegin);
  538. TraceAssert(NULL != pszEnd);
  539. TraceAssert(pszBegin >= pszPath);
  540. TraceAssert(pszBegin <= (pszPath + lstrlen(pszPath)));
  541. TraceAssert(pszEnd >= pszPath);
  542. TraceAssert(pszEnd <= (pszPath + lstrlen(pszPath)));
  543. TraceAssert(TEXT('\\') != *pszBegin);
  544. return pszBegin;
  545. }
  546. //
  547. // Recursively adds components of a path string to the tree.
  548. //
  549. HRESULT
  550. CNoPinList::CNode::AddPath(
  551. LPTSTR pszPath
  552. )
  553. {
  554. TraceAssert(NULL != pszPath);
  555. HRESULT hr = NOPIN_E_BADPATH;
  556. if (NULL != pszPath)
  557. {
  558. hr = S_OK;
  559. int cchPart = 0;
  560. LPTSTR pszPart = (LPTSTR)_FindNextPathComponent(pszPath, &cchPart);
  561. if (*pszPart)
  562. {
  563. TCHAR chTemp = TEXT('\0');
  564. _SwapChars(&chTemp, pszPart + cchPart);
  565. CNode *pChild = _FindChild(pszPart);
  566. if (NULL != pChild)
  567. {
  568. //
  569. // Found an existing node for this part of the path.
  570. // If the node has children, give the remainder of the path
  571. // to this node for addition. If it doesn't that means
  572. // it's a leaf node and all it's children are excluded from
  573. // pinning. No reason to add any children to it.
  574. //
  575. _SwapChars(&chTemp, pszPart + cchPart);
  576. if (pChild->HasChildren())
  577. {
  578. hr = pChild->AddPath(pszPart + cchPart);
  579. }
  580. }
  581. else
  582. {
  583. //
  584. // This is a new sub-path that is not yet in the tree.
  585. //
  586. hr = E_OUTOFMEMORY;
  587. pChild = new CNode();
  588. if (NULL != pChild)
  589. {
  590. //
  591. // Initialize the new child.
  592. //
  593. hr = pChild->Initialize(pszPart);
  594. _SwapChars(&chTemp, pszPart + cchPart);
  595. if (SUCCEEDED(hr))
  596. {
  597. //
  598. // Have the new child add the remainder of
  599. // the path as it's children.
  600. //
  601. hr = pChild->AddPath(pszPart + cchPart);
  602. if (SUCCEEDED(hr))
  603. {
  604. //
  605. // Link the new child into the list of children.
  606. //
  607. _AddChild(pChild);
  608. }
  609. }
  610. if (FAILED(hr))
  611. {
  612. delete pChild;
  613. pChild = NULL;
  614. }
  615. }
  616. }
  617. }
  618. else
  619. {
  620. //
  621. // We're at the end of the path, that means we're at a leaf node
  622. // and this file or directory is excluded from pinning. If it's
  623. // a directory, all children are excluded from pinning so there's
  624. // no reason to keep any child nodes in the tree. This keeps the
  625. // tree trimmed to a minimum necessary size.
  626. //
  627. delete m_pChildren;
  628. m_pChildren = NULL;
  629. }
  630. }
  631. TraceAssert(S_OK == hr ||
  632. E_OUTOFMEMORY == hr ||
  633. NOPIN_E_BADPATH == hr);
  634. return hr;
  635. }
  636. //
  637. // Recursively determines if a given complete subpath exists for a
  638. // path string. If a match occurs at a given level in the tree,
  639. // the remainder of the path string is given to the matching node
  640. // for further searching. This process continues recursively until
  641. // we hit a leaf node in the tree or the end of the path string,
  642. // whichever occurs first.
  643. //
  644. // Returns:
  645. // S_OK - A complete path exists that is a subpath of pszPath.
  646. // S_FALSE - A complete path does not exist.
  647. //
  648. HRESULT
  649. CNoPinList::CNode::SubPathExists(
  650. LPTSTR pszPath
  651. ) const
  652. {
  653. HRESULT hr = NOPIN_E_BADPATH;
  654. if (NULL != pszPath)
  655. {
  656. hr = S_FALSE;
  657. int cchPart = 0;
  658. LPTSTR pszPart = (LPTSTR)_FindNextPathComponent(pszPath, &cchPart);
  659. if (*pszPart)
  660. {
  661. TCHAR chTemp = TEXT('\0');
  662. _SwapChars(&chTemp, pszPart + cchPart);
  663. CNode *pChild = _FindChild(pszPart);
  664. _SwapChars(&chTemp, pszPart + cchPart);
  665. if (NULL != pChild)
  666. {
  667. if (pChild->HasChildren())
  668. {
  669. hr = pChild->SubPathExists(pszPart + cchPart);
  670. }
  671. else
  672. {
  673. //
  674. // Hit a leaf node. That means that we've traversed
  675. // down a complete subpath of the path in question.
  676. // Pinning of this path is not allowed.
  677. //
  678. hr = S_OK;
  679. }
  680. }
  681. }
  682. }
  683. TraceAssert(S_OK == hr ||
  684. S_FALSE == hr ||
  685. NOPIN_E_BADPATH == hr);
  686. return hr;
  687. }
  688. #if DBG
  689. //
  690. // This function dumps the contents of a tree node and all it's decendents.
  691. // The result is an indented list of nodes in the debugger output.
  692. // Handy for debugging tree build problems.
  693. //
  694. void
  695. CNoPinList::_DumpNode(
  696. const CNoPinList::CNode *pNode,
  697. int iIndent
  698. )
  699. {
  700. CNodeInspector ni(pNode);
  701. TCHAR szText[1024] = {0};
  702. iIndent = min(iIndent, 60);
  703. LPTSTR pszWrite = szText;
  704. UINT cchWrite = ARRAYSIZE(szText);
  705. for (int i = 0; i < iIndent; i++)
  706. {
  707. *pszWrite++ = TEXT(' ');
  708. cchWrite--;
  709. }
  710. ::OutputDebugString(TEXT("\n\r"));
  711. ::wnsprintf(pszWrite, cchWrite, TEXT("Node Address.: 0x%08X\n\r"), pNode);
  712. ::OutputDebugString(szText);
  713. ::wnsprintf(pszWrite, cchWrite, TEXT("Name.........: %s\n\r"), ni.NodeName() ? ni.NodeName() : TEXT("<null>"));
  714. ::OutputDebugString(szText);
  715. ::wnsprintf(pszWrite, cchWrite, TEXT("Children.....: 0x%08X\n\r"), ni.ChildList());
  716. ::OutputDebugString(szText);
  717. ::wnsprintf(pszWrite, cchWrite, TEXT("Next Sibling.: 0x%08X\n\r"), ni.NextSibling());
  718. ::OutputDebugString(szText);
  719. if (NULL != ni.ChildList())
  720. {
  721. _DumpNode(ni.ChildList(), iIndent + 5);
  722. }
  723. if (NULL != ni.NextSibling())
  724. {
  725. _DumpNode(ni.NextSibling(), iIndent);
  726. }
  727. }
  728. //
  729. // Dump the entire tree starting with the root.
  730. //
  731. void
  732. CNoPinList::Dump(
  733. void
  734. )
  735. {
  736. ::OutputDebugString(TEXT("\n\rDumping CNoPinList\n\r"));
  737. if (NULL != m_pRoot)
  738. {
  739. _DumpNode(m_pRoot, 0);
  740. }
  741. }
  742. #endif // DBG