1 /*
2 * regexpl - Console Registry Explorer
3 *
4 * Copyright (C) 2000-2005 Nedko Arnaudov <nedko@users.sourceforge.net>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; see the file COPYING. If not, write to
18 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19 * Boston, MA 02111-1307, USA.
20 */
21
22 // RegistryTree.cpp: implementation of the CRegistryTree class.
23
24 #include "ph.h"
25 #include "RegistryTree.h"
26 #include "Pattern.h"
27 #include "RegistryExplorer.h"
28
CRegistryTree()29 CRegistryTree::CRegistryTree()
30 {
31 m_pszMachineName = NULL;
32 VERIFY(SUCCEEDED(m_Root.m_Key.InitRoot()));
33 m_Root.m_pUp = NULL;
34 m_pCurrentKey = &m_Root;
35 ASSERT(m_pCurrentKey->m_Key.IsRoot());
36 m_ErrorMsg[ERROR_MSG_BUFFER_SIZE] = 0;
37 }
38
CRegistryTree(const CRegistryTree & Tree)39 CRegistryTree::CRegistryTree(const CRegistryTree& Tree)
40 {
41 m_pszMachineName = NULL;
42 VERIFY(SUCCEEDED(m_Root.m_Key.InitRoot()));
43 m_Root.m_pUp = NULL;
44 m_pCurrentKey = &m_Root;
45 ASSERT(m_pCurrentKey->m_Key.IsRoot());
46
47 const TCHAR *pszPath = Tree.GetCurrentPath();
48 if ((pszPath[0] == _T('\\')) && (pszPath[1] == _T('\\')))
49 { // path has machine name
50 pszPath += 2;
51 while (*pszPath && (*pszPath != _T('\\')))
52 pszPath++;
53
54 ASSERT(*pszPath == _T('\\')); // if path begins with \\ it must be followed by machine name
55 }
56
57 if (Tree.m_pszMachineName)
58 SetMachineName(Tree.m_pszMachineName);
59
60 VERIFY(ChangeCurrentKey(pszPath));
61 }
62
~CRegistryTree()63 CRegistryTree::~CRegistryTree()
64 {
65 if (m_pszMachineName)
66 delete[] m_pszMachineName;
67
68 CNode *pNode;
69 while(m_pCurrentKey->m_pUp)
70 {
71 pNode = m_pCurrentKey;
72 m_pCurrentKey = m_pCurrentKey->m_pUp;
73 delete pNode;
74 }
75
76 // We are on root
77 ASSERT(m_pCurrentKey->m_Key.IsRoot());
78 ASSERT(m_pCurrentKey == &m_Root);
79 }
80
GetCurrentPath() const81 const TCHAR * CRegistryTree::GetCurrentPath() const
82 {
83 return m_pCurrentKey->m_Key.GetKeyName();
84 }
85
IsCurrentRoot()86 BOOL CRegistryTree::IsCurrentRoot()
87 {
88 return m_pCurrentKey->m_Key.IsRoot();
89 }
90
ChangeCurrentKey(const TCHAR * pszRelativePath)91 BOOL CRegistryTree::ChangeCurrentKey(const TCHAR *pszRelativePath)
92 {
93 if (*pszRelativePath == _T('\\'))
94 GotoRoot(); // This is full absolute path.
95
96 // split path to key names.
97 const TCHAR *pszSeps = _T("\\");
98
99 // Make buffer and copy relative path into it.
100 TCHAR *pszBuffer = new (std::nothrow) TCHAR[_tcslen(pszRelativePath)+1];
101 if (!pszBuffer)
102 {
103 SetError(ERROR_OUTOFMEMORY);
104 return FALSE;
105 }
106
107 _tcscpy(pszBuffer,pszRelativePath);
108
109 // We accept names in form "\"blablabla\\blab labla\"\\"
110 size_t size = _tcslen(pszBuffer);
111 if (size)
112 {
113 if (pszBuffer[size-1] == _T('\\'))
114 pszBuffer[--size] = 0;
115
116 TCHAR *psz;
117 if (*pszBuffer == _T('\"') && (psz = _tcschr(pszBuffer+1,_T('\"'))) && size_t(psz-pszBuffer) == size-1)
118 {
119 size--;
120 pszBuffer[size] = 0;
121
122 pszBuffer++;
123 }
124 }
125
126 const TCHAR *pszNewKey = _tcstok(pszBuffer,pszSeps);
127
128 if ((!pszNewKey)&&((*pszRelativePath != _T('\\'))||(*(pszRelativePath+1) != 0)))
129 {
130 SetError(_T("Invalid key name"));
131 goto Abort;
132 };
133
134 // change keys
135 while (pszNewKey)
136 {
137 if (!InternalChangeCurrentKey(pszNewKey,KEY_READ))
138 goto Abort; // InternalChangeCurrentKey sets last error description
139
140 // Get next key name
141 pszNewKey = _tcstok(NULL,pszSeps);
142 }
143
144 return TRUE;
145
146 Abort:
147 delete[] pszBuffer;
148 return FALSE;
149 }
150
GetLastErrorDescription()151 const TCHAR * CRegistryTree::GetLastErrorDescription()
152 {
153 return m_ErrorMsg;
154 }
155
GotoRoot()156 void CRegistryTree::GotoRoot()
157 {
158 // Delete current tree
159 CNode *pNode;
160 while(m_pCurrentKey->m_pUp)
161 {
162 pNode = m_pCurrentKey;
163 m_pCurrentKey = m_pCurrentKey->m_pUp;
164 delete pNode;
165 }
166
167 // We are on root
168 ASSERT(m_pCurrentKey->m_Key.IsRoot());
169 ASSERT(m_pCurrentKey == &m_Root);
170 }
171
SetMachineName(LPCTSTR pszMachineName)172 BOOL CRegistryTree::SetMachineName(LPCTSTR pszMachineName)
173 {
174 GotoRoot();
175
176 // If we are going to local machine...
177 if (pszMachineName == NULL)
178 {
179 // Delete previous machine name buffer if allocated.
180 if (m_pszMachineName)
181 delete[] m_pszMachineName;
182
183 m_pszMachineName = NULL;
184 m_Root.m_Key.InitRoot();
185 return TRUE;
186 }
187
188 // Skip leading backslashes if any.
189 while ((*pszMachineName)&&(*pszMachineName == _T('\\')))
190 pszMachineName++;
191
192 ASSERT(*pszMachineName); // No machine name.
193
194 TCHAR *pszNewMachineName = new (std::nothrow) TCHAR[_tcslen(pszMachineName)+3]; // two leading backslashes + terminating null
195
196 if (!pszNewMachineName)
197 {
198 SetError(ERROR_OUTOFMEMORY);
199 return FALSE;
200 }
201
202 // Delete previous machine name buffer if allocated.
203 if (m_pszMachineName)
204 delete[] m_pszMachineName;
205
206 m_pszMachineName = pszNewMachineName;
207
208 _tcscpy(m_pszMachineName,_T("\\\\")); // leading backslashes
209 _tcscpy(m_pszMachineName+2,pszMachineName); // machine name itself
210 _tcsupr(m_pszMachineName+2); // upercase it
211
212 VERIFY(SUCCEEDED(m_Root.m_Key.InitRoot(m_pszMachineName)));
213 return TRUE;
214 }
215
NewKey(const TCHAR * pszKeyName,const TCHAR * pszPath,BOOL blnVolatile)216 BOOL CRegistryTree::NewKey(const TCHAR *pszKeyName, const TCHAR *pszPath, BOOL blnVolatile)
217 {
218 if (!m_pCurrentKey)
219 {
220 SetErrorCommandNAOnRoot(_T("Creating new key "));
221 return FALSE;
222 }
223
224 CRegistryTree Tree(*this);
225 if (!Tree.ChangeCurrentKey(pszPath))
226 {
227 SetError(Tree.GetLastErrorDescription());
228 return FALSE;
229 }
230
231 BOOL blnOpened;
232 HKEY hKey;
233
234 LONG nError = Tree.m_pCurrentKey->m_Key.CreateSubkey(KEY_READ,
235 pszKeyName,
236 hKey,
237 &blnOpened,
238 blnVolatile);
239 if (nError == ERROR_SUCCESS)
240 {
241 LONG nError = RegCloseKey(hKey);
242 ASSERT(nError == ERROR_SUCCESS);
243 }
244
245 if ((nError == ERROR_SUCCESS) && blnOpened)
246 {
247 SetError(_T("A key \"%s\" already exists."),pszKeyName);
248 return FALSE;
249 }
250
251 if (nError != ERROR_SUCCESS)
252 {
253 SetError(_T("Cannot create key : %s%s\nError %d (%s)\n"),
254 GetCurrentPath(),pszKeyName,nError,GetErrorDescription(nError));
255
256 return FALSE;
257 }
258
259 return TRUE;
260 }
261
DeleteSubkeys(const TCHAR * pszKeyPattern,const TCHAR * pszPath,BOOL blnRecursive)262 BOOL CRegistryTree::DeleteSubkeys(const TCHAR *pszKeyPattern, const TCHAR *pszPath, BOOL blnRecursive)
263 {
264 CRegistryKey Key;
265 if (!GetKey(pszPath,KEY_QUERY_VALUE|KEY_ENUMERATE_SUB_KEYS|DELETE,Key))
266 return FALSE;
267
268 return DeleteSubkeys(Key, pszKeyPattern, blnRecursive);
269 }
270
DeleteSubkeys(CRegistryKey & rKey,const TCHAR * pszKeyPattern,BOOL blnRecursive)271 BOOL CRegistryTree::DeleteSubkeys(CRegistryKey& rKey, const TCHAR *pszKeyPattern, BOOL blnRecursive)
272 {
273 LONG nError;
274
275 // enumerate subkeys
276 DWORD dwMaxSubkeyNameLength;
277 nError = rKey.GetSubkeyNameMaxLength(dwMaxSubkeyNameLength);
278 if (nError != ERROR_SUCCESS)
279 {
280 SetError(_T("Cannot delete subkeys(s) of key %s.\nRequesting info about key failed.\nError %d (%s)\n"),
281 rKey.GetKeyName(),nError,GetErrorDescription(nError));
282 return FALSE;
283 }
284
285 TCHAR *pszSubkeyName = new (std::nothrow) TCHAR [dwMaxSubkeyNameLength];
286 rKey.InitSubkeyEnumeration(pszSubkeyName, dwMaxSubkeyNameLength);
287 BOOL blnKeyDeleted = FALSE;
288 while ((nError = rKey.GetNextSubkeyName()) == ERROR_SUCCESS)
289 {
290 if (PatternMatch(pszKeyPattern,pszSubkeyName))
291 {
292 if (blnRecursive)
293 { // deltion is recursive, delete subkey subkeys
294 CRegistryKey Subkey;
295 // open subkey
296 nError = rKey.OpenSubkey(DELETE,pszSubkeyName,Subkey);
297 // delete subkey subkeys
298 if (DeleteSubkeys(Subkey, PATTERN_MATCH_ALL, TRUE))
299 {
300 AddErrorDescription(_T("Cannot delete subkey(s) of key %s. Subkey deletion failed.\n"),Subkey.GetKeyName());
301 return FALSE;
302 }
303 }
304
305 nError = rKey.DeleteSubkey(pszSubkeyName);
306 if (nError != ERROR_SUCCESS)
307 {
308 SetError(_T("Cannot delete the %s subkey of key %s.\nError %d (%s)\n"),
309 pszSubkeyName,rKey.GetKeyName(),nError,GetErrorDescription(nError));
310
311 return FALSE;
312 }
313 blnKeyDeleted = TRUE;
314 rKey.InitSubkeyEnumeration(pszSubkeyName, dwMaxSubkeyNameLength); // reset iteration
315 }
316 }
317
318 ASSERT(nError != ERROR_SUCCESS);
319 if (nError != ERROR_NO_MORE_ITEMS)
320 {
321 SetError(_T("Cannot delete subkeys(s) of key %s.\nSubkey enumeration failed.\nError %d (%s)\n"),
322 rKey.GetKeyName(),nError,GetErrorDescription(nError));
323 return FALSE;
324 }
325
326 if (!blnKeyDeleted)
327 SetError(_T("The key %s has no subkeys that match %s pattern.\n"),rKey.GetKeyName(),pszKeyPattern);
328
329 return blnKeyDeleted;
330 }
331
GetErrorDescription(LONG nError)332 const TCHAR * CRegistryTree::GetErrorDescription(LONG nError)
333 {
334 switch(nError)
335 {
336 case ERROR_ACCESS_DENIED:
337 return _T("Access denied");
338 case ERROR_FILE_NOT_FOUND:
339 return _T("The system cannot find the key specified");
340 case ERROR_INTERNAL_ERROR:
341 return _T("Internal error");
342 case ERROR_OUTOFMEMORY:
343 return _T("Out of memory");
344 default:
345 return _T("Unknown error");
346 }
347 }
348
SetError(LONG nError)349 void CRegistryTree::SetError(LONG nError)
350 {
351 SetError(_T("Error %u (%s)"),nError,GetErrorDescription(nError));
352 }
353
SetError(const TCHAR * pszFormat,...)354 void CRegistryTree::SetError(const TCHAR *pszFormat, ...)
355 {
356 va_list args;
357 va_start(args,pszFormat);
358 if (_vsntprintf(m_ErrorMsg,ERROR_MSG_BUFFER_SIZE,pszFormat,args) < 0)
359 m_ErrorMsg[ERROR_MSG_BUFFER_SIZE] = 0;
360 va_end(args);
361 }
362
SetInternalError()363 void CRegistryTree::SetInternalError()
364 {
365 SetError(_T("Internal Error"));
366 }
367
AddErrorDescription(const TCHAR * pszFormat,...)368 void CRegistryTree::AddErrorDescription(const TCHAR *pszFormat, ...)
369 {
370 size_t size = _tcslen(m_ErrorMsg);
371 if (size < ERROR_MSG_BUFFER_SIZE)
372 {
373 TCHAR *pszAdd = m_ErrorMsg+size;
374 va_list args;
375 va_start(args,pszFormat);
376 size = ERROR_MSG_BUFFER_SIZE-size;
377 if (_vsntprintf(pszAdd,size,pszFormat,args) < 0)
378 m_ErrorMsg[size] = 0;
379 va_end(args);
380 }
381 }
382
SetErrorCommandNAOnRoot(const TCHAR * pszCommand)383 void CRegistryTree::SetErrorCommandNAOnRoot(const TCHAR *pszCommand)
384 {
385 ASSERT(pszCommand);
386 if (pszCommand)
387 SetError(_T("%s") COMMAND_NA_ON_ROOT,pszCommand);
388 else
389 SetInternalError();
390 }
391
InternalChangeCurrentKey(const TCHAR * pszSubkeyName,REGSAM DesiredAccess)392 BOOL CRegistryTree::InternalChangeCurrentKey(const TCHAR *pszSubkeyName, REGSAM DesiredAccess)
393 {
394 size_t size = _tcslen(pszSubkeyName);
395 TCHAR *pszSubkeyNameBuffer = new (std::nothrow) TCHAR[size+3];
396 if (!pszSubkeyNameBuffer)
397 {
398 SetError(_T("Cannot open key : %s%s\nError %d (%s)\n"),
399 GetCurrentPath(),pszSubkeyName,ERROR_OUTOFMEMORY,GetErrorDescription(ERROR_OUTOFMEMORY));
400 }
401
402 _tcscpy(pszSubkeyNameBuffer,pszSubkeyName);
403 if (size && (pszSubkeyName[0] == _T('\"')) && (pszSubkeyName[size-1] == _T('\"')))
404 {
405 pszSubkeyNameBuffer[size-1] = 0;
406 pszSubkeyName = pszSubkeyNameBuffer+1;
407 }
408
409 if (_tcscmp(pszSubkeyName,_T(".")) == 0)
410 {
411 delete[] pszSubkeyNameBuffer;
412 return TRUE;
413 }
414
415 if (_tcscmp(pszSubkeyName,_T("..")) == 0)
416 {
417 // Up level abstraction
418 if (m_pCurrentKey->m_Key.IsRoot())
419 {
420 // We are on root
421 ASSERT(m_pCurrentKey->m_pUp == NULL);
422 SetError(_T("Cannot open key. The root is not child.\n"));
423 delete[] pszSubkeyNameBuffer;
424 return FALSE;
425 }
426
427 ASSERT(m_pCurrentKey->m_pUp);
428 if (!m_pCurrentKey->m_pUp)
429 {
430 SetInternalError();
431 delete[] pszSubkeyNameBuffer;
432 return FALSE;
433 }
434 CNode *pNode = m_pCurrentKey;
435 m_pCurrentKey = m_pCurrentKey->m_pUp;
436 delete pNode;
437 delete[] pszSubkeyNameBuffer;
438 return TRUE;
439 }
440
441 CNode *pNewKey = new CNode;
442 if (!pNewKey)
443 {
444 SetError(_T("Cannot open key : %s%s\nError %d (%s)\n"),
445 GetCurrentPath(),pszSubkeyName,ERROR_OUTOFMEMORY,GetErrorDescription(ERROR_OUTOFMEMORY));
446 delete[] pszSubkeyNameBuffer;
447 return FALSE;
448 }
449
450 if (!InternalGetSubkey(pszSubkeyName,DesiredAccess,pNewKey->m_Key))
451 {
452 delete pNewKey;
453 delete[] pszSubkeyNameBuffer;
454 return FALSE;
455 }
456 pNewKey->m_pUp = m_pCurrentKey;
457 m_pCurrentKey = pNewKey;
458
459 delete[] pszSubkeyNameBuffer;
460 return TRUE;
461 }
462
InternalGetSubkey(const TCHAR * pszSubkeyName,REGSAM DesiredAccess,CRegistryKey & rKey)463 BOOL CRegistryTree::InternalGetSubkey(const TCHAR *pszSubkeyName, REGSAM DesiredAccess, CRegistryKey& rKey)
464 {
465 LONG nError;
466 HKEY hNewKey = NULL;
467 TCHAR *pszSubkeyNameCaseUpdated = NULL;
468
469 nError = m_pCurrentKey->m_Key.OpenSubkey(DesiredAccess,pszSubkeyName,hNewKey);
470
471 if (nError != ERROR_SUCCESS)
472 {
473 SetError(_T("Cannot open key : %s%s\nError %u (%s)\n"),
474 GetCurrentPath(),pszSubkeyName,nError,GetErrorDescription(nError));
475
476 return FALSE;
477 }
478
479 // enum subkeys to find the subkey and get its name in stored case.
480 DWORD dwMaxSubkeyNameLength;
481 nError = m_pCurrentKey->m_Key.GetSubkeyNameMaxLength(dwMaxSubkeyNameLength);
482 if (nError != ERROR_SUCCESS)
483 goto SkipCaseUpdate;
484
485 pszSubkeyNameCaseUpdated = new (std::nothrow) TCHAR [dwMaxSubkeyNameLength];
486 m_pCurrentKey->m_Key.InitSubkeyEnumeration(pszSubkeyNameCaseUpdated, dwMaxSubkeyNameLength);
487 while ((nError = m_pCurrentKey->m_Key.GetNextSubkeyName()) == ERROR_SUCCESS)
488 if (_tcsicmp(pszSubkeyNameCaseUpdated, pszSubkeyName) == 0)
489 break;
490
491 if (nError != ERROR_SUCCESS)
492 {
493 delete[] pszSubkeyNameCaseUpdated;
494 pszSubkeyNameCaseUpdated = NULL;
495 }
496
497 SkipCaseUpdate:
498
499 HRESULT hr;
500 ASSERT(hNewKey);
501 if (pszSubkeyNameCaseUpdated)
502 {
503 hr = rKey.Init(hNewKey,GetCurrentPath(),pszSubkeyNameCaseUpdated,DesiredAccess);
504 if (FAILED(hr))
505 {
506 if (hr == (HRESULT)E_OUTOFMEMORY)
507 SetError(_T("Cannot open key : %s%s\nError %d (%s)\n"),
508 GetCurrentPath(),pszSubkeyName,ERROR_OUTOFMEMORY,GetErrorDescription(ERROR_OUTOFMEMORY));
509 else
510 SetError(_T("Cannot open key : %s%s\nUnknown error\n"), GetCurrentPath(), pszSubkeyName);
511
512 goto Abort;
513 }
514
515 delete[] pszSubkeyNameCaseUpdated;
516 }
517 else
518 {
519 hr = rKey.Init(hNewKey,GetCurrentPath(),pszSubkeyName,DesiredAccess);
520 if (FAILED(hr))
521 {
522 if (hr == (HRESULT)E_OUTOFMEMORY)
523 SetError(_T("Cannot open key : %s%s\nError %d (%s)\n"),
524 GetCurrentPath(),pszSubkeyName,ERROR_OUTOFMEMORY,GetErrorDescription(ERROR_OUTOFMEMORY));
525 else
526 SetError(_T("Cannot open key : %s%s\nUnknown error \n"),
527 GetCurrentPath(),
528 pszSubkeyName);
529
530 goto Abort;
531 }
532 }
533
534 return TRUE;
535 Abort:
536 if (pszSubkeyNameCaseUpdated)
537 delete[] pszSubkeyNameCaseUpdated;
538
539 if (hNewKey)
540 {
541 LONG nError = RegCloseKey(hNewKey);
542 ASSERT(nError == ERROR_SUCCESS);
543 }
544
545 return FALSE;
546 }
547
GetKey(const TCHAR * pszRelativePath,REGSAM DesiredAccess,CRegistryKey & rKey)548 BOOL CRegistryTree::GetKey(const TCHAR *pszRelativePath, REGSAM DesiredAccess, CRegistryKey& rKey)
549 {
550 CRegistryTree Tree(*this);
551
552 if (!Tree.ChangeCurrentKey(pszRelativePath))
553 {
554 SetError(Tree.GetLastErrorDescription());
555 return FALSE;
556 }
557
558 if (Tree.m_pCurrentKey->m_Key.IsRoot())
559 {
560 HRESULT hr = rKey.InitRoot(m_pszMachineName);
561 if (FAILED(hr))
562 {
563 if (hr == (HRESULT)E_OUTOFMEMORY)
564 SetError(_T("\nError %d (%s)\n"),
565 ERROR_OUTOFMEMORY,GetErrorDescription(ERROR_OUTOFMEMORY));
566 else
567 SetInternalError();
568 return FALSE;
569 }
570
571 return TRUE;
572 }
573
574 // open key with desired access
575
576 // may be call to DuplicateHandle() is better.
577 // registry key handles returned by the RegConnectRegistry function cannot be used in a call to DuplicateHandle.
578
579 // Get short key name now...
580 const TCHAR *pszKeyName = Tree.m_pCurrentKey->m_Key.GetKeyName();
581 if (pszKeyName == NULL)
582 {
583 SetInternalError();
584 return FALSE;
585 }
586
587 size_t size = _tcslen(pszKeyName);
588 ASSERT(size);
589 if (!size)
590 {
591 SetInternalError();
592 return FALSE;
593 }
594
595 const TCHAR *pszShortKeyName_ = pszKeyName + size-1;
596 pszShortKeyName_--; // skip ending backslash
597 size = 0;
598 while (pszShortKeyName_ >= pszKeyName)
599 {
600 if (*pszShortKeyName_ == _T('\\'))
601 break;
602 pszShortKeyName_--;
603 size++;
604 }
605
606 if (!size || (*pszShortKeyName_ != _T('\\')))
607 {
608 ASSERT(FALSE);
609 SetInternalError();
610 return FALSE;
611 }
612
613 TCHAR *pszShortKeyName = new (std::nothrow) TCHAR [size+1];
614 if (!pszShortKeyName)
615 {
616 SetError(ERROR_OUTOFMEMORY);
617 return FALSE;
618 }
619
620 memcpy(pszShortKeyName,pszShortKeyName_+1,size*sizeof(TCHAR));
621 pszShortKeyName[size] = 0;
622
623 // change to parent key
624 if (!Tree.InternalChangeCurrentKey(_T(".."),READ_CONTROL))
625 {
626 delete[] pszShortKeyName;
627 ASSERT(FALSE);
628 SetInternalError();
629 return FALSE;
630 }
631
632 // change back to target key
633 if (!Tree.InternalGetSubkey(pszShortKeyName,DesiredAccess,rKey))
634 {
635 delete[] pszShortKeyName;
636 SetError(Tree.GetLastErrorDescription());
637 return FALSE;
638 }
639
640 delete[] pszShortKeyName;
641 return TRUE;
642 }
643
644