mirror of https://github.com/tongzx/nt5src
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.
717 lines
18 KiB
717 lines
18 KiB
//+---------------------------------------------------------------------------
|
|
//
|
|
// Microsoft Windows
|
|
// Copyright (C) Microsoft Corporation, 1997.
|
|
//
|
|
// File: N C R E G 2 . C P P
|
|
//
|
|
// Contents: Common routines for dealing with the registry.
|
|
//
|
|
// Notes:
|
|
//
|
|
// Author: CWill 27 Mar 1997
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
|
|
#include <pch.h>
|
|
#pragma hdrstop
|
|
#include "ncerror.h"
|
|
#include "ncipaddr.h"
|
|
#include "ncmem.h"
|
|
#include "ncreg.h"
|
|
#include "ncstring.h"
|
|
#include <regstr.h>
|
|
|
|
static const WCHAR c_szSubkeyServices[] = REGSTR_PATH_SERVICES;
|
|
|
|
const struct REG_TYPE_MAP
|
|
{
|
|
DWORD dwPsuedoType;
|
|
DWORD dwType;
|
|
};
|
|
|
|
REG_TYPE_MAP c_rgrtmTypes[] =
|
|
{
|
|
{REG_BOOL, REG_DWORD},
|
|
{REG_IP, REG_SZ},
|
|
};
|
|
|
|
|
|
|
|
DWORD DwRealTypeFromPsuedoType(const DWORD dwPsuedoType)
|
|
{
|
|
for (UINT cLoop = 0; cLoop < celems(c_rgrtmTypes); cLoop++)
|
|
{
|
|
if (dwPsuedoType == c_rgrtmTypes[cLoop].dwPsuedoType)
|
|
{
|
|
return c_rgrtmTypes[cLoop].dwType;
|
|
}
|
|
}
|
|
|
|
return dwPsuedoType;
|
|
}
|
|
|
|
struct SPECIAL_KEY_MAP
|
|
{
|
|
HKEY hkeyPseudo;
|
|
HKEY hkeyRoot;
|
|
PCWSTR pszSubKey;
|
|
};
|
|
|
|
static const SPECIAL_KEY_MAP c_rgskmSpec[] =
|
|
{
|
|
HKLM_SVCS, HKEY_LOCAL_MACHINE, c_szSubkeyServices,
|
|
};
|
|
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: HkeyTrueParent
|
|
//
|
|
// Purpose: To get a real handle to a key from a pseudo handle
|
|
//
|
|
// Arguments:
|
|
// hkeyIn The pseudo key name
|
|
// samDesired The access requested of the key
|
|
// rghkeySpec An array of the special keys.
|
|
//
|
|
// Returns: The handle to the opened key or NULL
|
|
//
|
|
// Author: CWill Apr 30 1997
|
|
//
|
|
// Notes:
|
|
//
|
|
HKEY HkeyTrueParent(const HKEY hkeyIn, const REGSAM samDesired,
|
|
HKEY rghkeySpec[])
|
|
{
|
|
HKEY hkeyRet = hkeyIn;
|
|
|
|
for (UINT cKey = 0; cKey < celems(c_rgskmSpec); cKey++)
|
|
{
|
|
// Check arb->hkey for one of "our" well known keys.
|
|
if (c_rgskmSpec[cKey].hkeyPseudo == hkeyIn)
|
|
{
|
|
if (!rghkeySpec[cKey])
|
|
{
|
|
// First time a special key was used. We need to cache it.
|
|
#ifdef DBG
|
|
HRESULT hr =
|
|
#endif // DBG
|
|
HrRegOpenKeyEx(
|
|
c_rgskmSpec[cKey].hkeyRoot,
|
|
c_rgskmSpec[cKey].pszSubKey,
|
|
samDesired,
|
|
&rghkeySpec[cKey]);
|
|
|
|
// If we fail to open the key, make sure the output
|
|
// parameter was nulled. This will allow us to proceed
|
|
// without really handling the error as hkeyParent
|
|
// will be set to null below and the following
|
|
// HrRegOpenKey will fail. We will then handle the failure
|
|
// of that.
|
|
AssertSz(FImplies(FAILED(hr), !rghkeySpec[cKey]), "Key not NULL");
|
|
}
|
|
|
|
hkeyRet = rghkeySpec[cKey];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return hkeyRet;
|
|
}
|
|
|
|
|
|
|
|
VOID RegSafeCloseKeyArray(HKEY rghkey[], UINT cElems)
|
|
{
|
|
for (UINT cKey = 0; cKey < cElems; cKey++)
|
|
{
|
|
RegSafeCloseKey(rghkey[cKey]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: TranslateFromRegToData
|
|
//
|
|
// Purpose: Translates the data retrieved from the registry to a the user
|
|
// data's storage format
|
|
//
|
|
// Arguments:
|
|
// dwType The registry pseudo type that is being translated
|
|
// pbData Where the data gets stored
|
|
// pbBuf A buffer that stores the registry data
|
|
//
|
|
// Returns: Nothing.
|
|
//
|
|
// Author: CWill Apr 30 1997
|
|
//
|
|
// Notes:
|
|
//
|
|
VOID TranslateFromRegToData(DWORD dwType, BYTE* pbData, BYTE* pbBuf)
|
|
{
|
|
// Take the data from the registry and happily convert it into
|
|
// usable data
|
|
switch (dwType)
|
|
{
|
|
|
|
#ifdef DBG
|
|
default:
|
|
{
|
|
AssertSz(FALSE, "Unknown registry type");
|
|
break;
|
|
}
|
|
#endif // DBG
|
|
|
|
case REG_IP:
|
|
{
|
|
// Convert the stringized form of the ip address
|
|
// into a DWORD. (The actual 32-bit IP address.)
|
|
DWORD dwIpAddr = IpPszToHostAddr((WCHAR*)pbBuf);
|
|
*((DWORD*)pbData) = dwIpAddr;
|
|
break;
|
|
}
|
|
|
|
case REG_BOOL:
|
|
{
|
|
// Form the boolean as 'TRUE' or 'FALSE' based on
|
|
// whether the data is non-zero or zero respectively.
|
|
DWORD dwData = *((DWORD*)pbBuf);
|
|
*((BOOL*)pbData) = (!!dwData);
|
|
break;
|
|
}
|
|
|
|
case REG_DWORD:
|
|
{
|
|
// DWORDs are direct assignments
|
|
*((DWORD*)pbData) = *((DWORD*)pbBuf);
|
|
break;
|
|
}
|
|
|
|
case REG_SZ:
|
|
{
|
|
// Make a copy of the string
|
|
*((PWSTR*) pbData) = SzDupSz((PWSTR)pbBuf);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
inline VOID UseDefaultRegValue(DWORD dwType, BYTE* pbData, BYTE* pbDefault)
|
|
{
|
|
AssertSz((pbData && pbDefault), "UseDefaultRegValue : Invalid params");
|
|
AssertSz(pbDefault, "There is no default registry value");
|
|
|
|
TranslateFromRegToData(dwType, pbData, pbDefault);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: CbSizeOfDataToReg
|
|
//
|
|
// Purpose: To determine the size of buffer needed to store the data
|
|
//
|
|
// Arguments:
|
|
// dwType The registry pseudo type that is being translated
|
|
// pbData The data that has to be translated
|
|
//
|
|
// Returns: The size of buffer need to store the data
|
|
//
|
|
// Author: CWill Apr 30 1997
|
|
//
|
|
// Notes:
|
|
//
|
|
DWORD CbSizeOfDataToReg(DWORD dwType, const BYTE* pbData)
|
|
{
|
|
DWORD cbData = 0;
|
|
|
|
switch (dwType)
|
|
{
|
|
#ifdef DBG
|
|
default:
|
|
{
|
|
AssertSz(FALSE, "Unknown registry type");
|
|
break;
|
|
}
|
|
#endif // DBG
|
|
|
|
case REG_IP:
|
|
{
|
|
// Convert the 32-bit IP address to a stringized form.
|
|
DWORD dwIpAddr = *((DWORD*)pbData);
|
|
|
|
WCHAR pszIpAddr [32];
|
|
IpHostAddrToPsz(dwIpAddr, pszIpAddr);
|
|
|
|
cbData = CbOfSzAndTerm(pszIpAddr);
|
|
break;
|
|
}
|
|
|
|
// Boolean values are stored as DWORDs
|
|
case REG_BOOL:
|
|
case REG_DWORD:
|
|
{
|
|
cbData = sizeof(DWORD);
|
|
break;
|
|
}
|
|
|
|
case REG_SZ:
|
|
case REG_EXPAND_SZ:
|
|
{
|
|
cbData = CbOfSzAndTerm(*((PCWSTR*)pbData));
|
|
break;
|
|
}
|
|
}
|
|
|
|
AssertSz(cbData, "We should have a size");
|
|
|
|
return cbData;
|
|
}
|
|
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: TranslateFromDataToReg
|
|
//
|
|
// Purpose: Translates user data to a format the can be stored in the
|
|
// registry
|
|
//
|
|
// Arguments:
|
|
// dwType The registry pseudo type that is being translated
|
|
// pbData The data that has to be translated
|
|
// pbBuf A buffer that stores the registry data
|
|
//
|
|
// Returns: Nothing.
|
|
//
|
|
// Author: CWill Apr 30 1997
|
|
//
|
|
// Notes:
|
|
//
|
|
VOID
|
|
TranslateFromDataToReg(
|
|
IN DWORD dwType,
|
|
IN const BYTE* pbData,
|
|
OUT const BYTE* pbBuf)
|
|
{
|
|
switch (dwType)
|
|
{
|
|
#ifdef DBG
|
|
default:
|
|
{
|
|
AssertSz(FALSE, "Unknown registry type");
|
|
break;
|
|
}
|
|
#endif // DBG
|
|
|
|
case REG_IP:
|
|
{
|
|
// Convert the 32-bit IP address to a stringized form.
|
|
DWORD dwIpAddr = *((DWORD*)pbData);
|
|
|
|
WCHAR pszIpAddr [32];
|
|
IpHostAddrToPsz (dwIpAddr, pszIpAddr);
|
|
|
|
// Copy the string
|
|
lstrcpyW((PWSTR)pbBuf, pszIpAddr);
|
|
break;
|
|
}
|
|
|
|
case REG_BOOL:
|
|
{
|
|
// Form the boolean as 'TRUE' or 'FALSE' based on
|
|
// whether the data is non-zero or zero respectively.
|
|
DWORD dwData = *((DWORD*)pbData);
|
|
*((BOOL*)pbBuf) = (!!dwData);
|
|
break;
|
|
}
|
|
|
|
case REG_DWORD:
|
|
{
|
|
// DWORDs are direct assignments
|
|
*((DWORD*)pbBuf) = *((DWORD*)pbData);
|
|
break;
|
|
}
|
|
|
|
case REG_SZ:
|
|
case REG_EXPAND_SZ:
|
|
{
|
|
// Make a copy of the string
|
|
lstrcpyW((PWSTR)pbBuf, *((PCWSTR*)pbData));
|
|
|
|
AssertSz(CbOfSzAndTerm(*((PCWSTR*)pbData)), "Zero length string");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: RegReadValues
|
|
//
|
|
// Purpose: To read a table of information from the registry into a user
|
|
// defined data structure.
|
|
//
|
|
// Arguments:
|
|
// crb The count of entries in the REGBATCH structure
|
|
// arb The pointer to the REGBATCH structure
|
|
// pbUserData The pointer to the source structure that is to retrieve
|
|
// the data from the registry
|
|
// samDesired The requested key access mask
|
|
//
|
|
// Returns: Nothing.
|
|
//
|
|
// Author: CWill Apr 30 1997
|
|
//
|
|
// Notes:
|
|
//
|
|
VOID RegReadValues(
|
|
IN INT crb,
|
|
IN const REGBATCH* arb,
|
|
OUT const BYTE* pbUserData,
|
|
IN REGSAM samDesired)
|
|
{
|
|
AssertSz(FImplies(crb, arb), "Count without an array");
|
|
|
|
HRESULT hr = S_OK;
|
|
const REGBATCH* prbLast = NULL;
|
|
HKEY rghkeySpec[celems(c_rgskmSpec)] = {0};
|
|
HKEY hkey = NULL;
|
|
|
|
while (crb--)
|
|
{
|
|
BYTE* pbData = (BYTE*)(pbUserData + arb->cbOffset);
|
|
|
|
// Open the key if we need to.
|
|
// We don't need to if it was the same as the previous one used.
|
|
if ((!prbLast )
|
|
|| (prbLast->hkey != arb->hkey)
|
|
|| (prbLast->pszSubkey != arb->pszSubkey))
|
|
{
|
|
HKEY hkeyParent;
|
|
|
|
hkeyParent = HkeyTrueParent (arb->hkey, samDesired, rghkeySpec);
|
|
|
|
// Close the previous key we used.
|
|
RegSafeCloseKey (hkey);
|
|
|
|
// Open the new key.
|
|
#ifdef DBG
|
|
hr =
|
|
#endif // DBG
|
|
HrRegOpenKeyEx (hkeyParent, arb->pszSubkey, samDesired, &hkey);
|
|
AssertSz(FImplies(FAILED(hr), !hkey), "HrRegOpenKey not NULLing");
|
|
}
|
|
|
|
// Only continue if we have a key.
|
|
if (hkey)
|
|
{
|
|
DWORD dwType = arb->dwType;
|
|
|
|
// We can't read NULL registry values
|
|
if (REG_CREATE != dwType)
|
|
{
|
|
DWORD cbData = 0;
|
|
BYTE* pbStack = NULL;
|
|
DWORD dwRealType = DwRealTypeFromPsuedoType(dwType);
|
|
|
|
// Ensure that we fail the first time around so that we can see how
|
|
// big a buffer is needed
|
|
(VOID) HrRegQueryValueEx(hkey, arb->pszValueName, &dwRealType,
|
|
NULL, &cbData);
|
|
|
|
// Allocate memory on the stack to serve as our temporary buffer.
|
|
#ifndef STACK_ALLOC_DOESNT_WORK
|
|
pbStack = (BYTE*)MemAlloc (cbData);
|
|
#else // !STACK_ALLOC_DOESNT_WORK
|
|
pbStack = (BYTE*)PvAllocOnStack(cbData);
|
|
#endif // !STACK_ALLOC_DOESNT_WORK
|
|
|
|
hr = HrRegQueryValueEx(hkey, arb->pszValueName, &dwRealType,
|
|
pbStack, &cbData);
|
|
|
|
if (S_OK == hr)
|
|
{
|
|
// Make sure its the type we were expecting.
|
|
AssertSz((dwRealType == DwRealTypeFromPsuedoType(dwType)),
|
|
"Value types do no match");
|
|
|
|
TranslateFromRegToData(dwType, pbData, pbStack);
|
|
}
|
|
else
|
|
{
|
|
UseDefaultRegValue(dwType, pbData, arb->pbDefault);
|
|
|
|
TraceHr (ttidError, FAL, hr,
|
|
HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr,
|
|
"RegReadValues: Could not open value %S", arb->pszValueName);
|
|
}
|
|
#ifndef STACK_ALLOC_DOESNT_WORK
|
|
MemFree (pbStack);
|
|
#endif // STACK_ALLOC_DOESNT_WORK
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TraceTag(ttidError, "RegReadValues: NULL key for %S", arb->pszSubkey);
|
|
UseDefaultRegValue(arb->dwType, pbData, arb->pbDefault);
|
|
}
|
|
|
|
// Advance prbLast or set it to the first one if this is the
|
|
// first time through.
|
|
if (prbLast)
|
|
{
|
|
prbLast++;
|
|
}
|
|
else
|
|
{
|
|
prbLast = arb;
|
|
}
|
|
|
|
arb++;
|
|
}
|
|
|
|
// Clean up
|
|
RegSafeCloseKey(hkey);
|
|
RegSafeCloseKeyArray(rghkeySpec, celems(rghkeySpec));
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: HrRegWriteValues
|
|
//
|
|
// Purpose: To write a table of information to the registry from a user
|
|
// defined data structure.
|
|
//
|
|
// Arguments:
|
|
// crb The count of entries in the REGBATCH structure
|
|
// arb The pointer to the REGBATCH structure
|
|
// pbUserData The pointer to the source structure that provides
|
|
// the data that is to be written to the registry
|
|
// dwOptions Options to be used when creating the registry keys
|
|
// samDesired The requested key access mask
|
|
//
|
|
// Returns: S_OK or an HRESULT_FROM_WIN32 error code.
|
|
//
|
|
// Author: CWill Apr 30 1997
|
|
//
|
|
// Notes:
|
|
//
|
|
HRESULT HrRegWriteValues(
|
|
INT crb,
|
|
const REGBATCH* arb,
|
|
const BYTE* pbUserData,
|
|
DWORD dwOptions,
|
|
REGSAM samDesired)
|
|
{
|
|
AssertSz(FImplies(crb, arb), "HrWriteValues : Count with no array");
|
|
|
|
HRESULT hr = S_OK;
|
|
const REGBATCH* prbLast = NULL;
|
|
HKEY hkey = NULL;
|
|
HKEY rghkeySpec[celems(c_rgskmSpec)] = {0};
|
|
|
|
while (crb--)
|
|
{
|
|
BYTE* pbData = const_cast<BYTE*>(pbUserData + arb->cbOffset);
|
|
|
|
// Open the key if we need to.
|
|
// We don't need to if it was the same as the previous one used.
|
|
if ((!prbLast)
|
|
|| (prbLast->hkey != arb->hkey)
|
|
|| (prbLast->pszSubkey != arb->pszSubkey))
|
|
{
|
|
HKEY hkeyParent;
|
|
|
|
hkeyParent = HkeyTrueParent(arb->hkey, samDesired, rghkeySpec);
|
|
|
|
// Close the previous key we used.
|
|
RegSafeCloseKey(hkey);
|
|
|
|
// Open the new key.
|
|
DWORD dwDisposition;
|
|
hr = HrRegCreateKeyEx(hkeyParent, arb->pszSubkey, dwOptions,
|
|
samDesired, NULL, &hkey, &dwDisposition);
|
|
|
|
AssertSz(FImplies(FAILED(hr), !hkey), "HrRegCreateKey not NULLing");
|
|
|
|
if (FAILED(hr))
|
|
{
|
|
TraceError("HrRegWriteValues: failed to open parent key", hr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Should definately have hkey by now.
|
|
AssertSz(hkey, "Why no key?");
|
|
|
|
//
|
|
// Format the data to be put into the registry
|
|
//
|
|
|
|
DWORD dwType = arb->dwType;
|
|
|
|
// If all we want to do is create the key, then we are already done.
|
|
if (REG_CREATE != dwType)
|
|
{
|
|
DWORD dwRealType = DwRealTypeFromPsuedoType(dwType);
|
|
DWORD cbReg = CbSizeOfDataToReg(dwType, pbData);
|
|
BYTE* pbReg = NULL;
|
|
|
|
AssertSz(cbReg, "We must have some data");
|
|
|
|
#ifndef STACK_ALLOC_DOESNT_WORK
|
|
pbReg = new BYTE[cbReg];
|
|
#else // !STACK_ALLOC_DOESNT_WORK
|
|
pbReg = reinterpret_cast<BYTE*>(PvAllocOnStack(cbReg));
|
|
#endif // !STACK_ALLOC_DOESNT_WORK
|
|
|
|
TranslateFromDataToReg(dwType, pbData, pbReg);
|
|
|
|
// Write the data to the registry.
|
|
hr = HrRegSetValueEx(
|
|
hkey,
|
|
arb->pszValueName,
|
|
dwRealType,
|
|
pbReg,
|
|
cbReg);
|
|
|
|
#ifndef STACK_ALLOC_DOESNT_WORK
|
|
// Must have this call before the break
|
|
delete [] pbReg;
|
|
#endif // STACK_ALLOC_DOESNT_WORK
|
|
|
|
}
|
|
|
|
if (FAILED(hr))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Advance prbLast or set it to the first one if this is the
|
|
// first time through.
|
|
if (prbLast)
|
|
{
|
|
prbLast++;
|
|
}
|
|
else
|
|
{
|
|
prbLast = arb;
|
|
}
|
|
|
|
arb++;
|
|
}
|
|
|
|
RegSafeCloseKey(hkey);
|
|
RegSafeCloseKeyArray(rghkeySpec, celems(rghkeySpec));
|
|
|
|
TraceError("HrWriteValues", hr);
|
|
return hr;
|
|
}
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Member: HrRegWriteValueTable
|
|
//
|
|
// Purpose: To write a table of values to the registry from a user
|
|
// defined data structure.
|
|
//
|
|
// Arguments:
|
|
// hkeyRoot The key to which the values are written
|
|
// cvt The count of entries in the VALUETABLE structure
|
|
// avt The pointer to the VALUETABLE structure
|
|
// pbUserData The pointer to the source structure that provides
|
|
// the data that is to be written to the registry
|
|
// dwOptions Options to be used when creating the registry keys
|
|
// samDesired The requested key access mask
|
|
//
|
|
// Returns: S_OK or an HRESULT_FROM_WIN32 error code.
|
|
//
|
|
// Author: CWill 06/26/97
|
|
//
|
|
// Notes:
|
|
//
|
|
HRESULT HrRegWriteValueTable(
|
|
HKEY hkeyRoot,
|
|
INT cvt,
|
|
const VALUETABLE* avt,
|
|
const BYTE* pbUserData,
|
|
DWORD dwOptions,
|
|
REGSAM samDesired)
|
|
{
|
|
HRESULT hr = S_OK;
|
|
|
|
while (cvt--)
|
|
{
|
|
BYTE* pbData = NULL;
|
|
DWORD dwType = REG_NONE;
|
|
|
|
//
|
|
// Format the data to be put into the registry
|
|
//
|
|
|
|
dwType = avt->dwType;
|
|
pbData = const_cast<BYTE*>(pbUserData + avt->cbOffset);
|
|
|
|
// If all we want to do is create the key, then we are already done.
|
|
if (REG_CREATE != dwType)
|
|
{
|
|
DWORD dwRealType = DwRealTypeFromPsuedoType(dwType);
|
|
DWORD cbReg = CbSizeOfDataToReg(dwType, pbData);
|
|
BYTE* pbReg = NULL;
|
|
|
|
AssertSz(cbReg, "We must have some data");
|
|
|
|
#ifndef STACK_ALLOC_DOESNT_WORK
|
|
pbReg = new BYTE[cbReg];
|
|
#else // !STACK_ALLOC_DOESNT_WORK
|
|
pbReg = reinterpret_cast<BYTE*>(PvAllocOnStack(cbReg));
|
|
#endif // !STACK_ALLOC_DOESNT_WORK
|
|
|
|
TranslateFromDataToReg(dwType, pbData, pbReg);
|
|
|
|
// Write the data to the registry.
|
|
hr = HrRegSetValueEx(
|
|
hkeyRoot,
|
|
avt->pszValueName,
|
|
dwRealType,
|
|
pbReg,
|
|
cbReg);
|
|
|
|
#ifndef STACK_ALLOC_DOESNT_WORK
|
|
// Must have this call before the break
|
|
delete [] pbReg;
|
|
#endif // STACK_ALLOC_DOESNT_WORK
|
|
}
|
|
|
|
if (FAILED(hr))
|
|
{
|
|
break;
|
|
}
|
|
|
|
avt++;
|
|
}
|
|
|
|
TraceError("HrRegWriteValueTable", hr);
|
|
return hr;
|
|
}
|
|
|