/****************************************************************** DfsJnPt.CPP -- WMI provider class implementation Copyright (c) 2000-2001 Microsoft Corporation, All Rights Reserved Description: Win32 Dfs Provider ******************************************************************/ #include "precomp.h" #include CDfsJnPt MyDfsTable ( PROVIDER_NAME_DFSJNPT , Namespace ) ; /***************************************************************************** * * FUNCTION : CDfsJnPt::CDfsJnPt * * DESCRIPTION : Constructor * *****************************************************************************/ CDfsJnPt :: CDfsJnPt ( LPCWSTR lpwszName, LPCWSTR lpwszNameSpace ) : Provider ( lpwszName , lpwszNameSpace ) { m_ComputerName = GetLocalComputerName(); } /***************************************************************************** * * FUNCTION : CDfsJnPt::~CDfsJnPt * * DESCRIPTION : Destructor * *****************************************************************************/ CDfsJnPt :: ~CDfsJnPt () { } /***************************************************************************** * * FUNCTION : CDfsJnPt::EnumerateInstances * * DESCRIPTION : Returns all the instances of this class. * *****************************************************************************/ HRESULT CDfsJnPt :: EnumerateInstances ( MethodContext *pMethodContext, long lFlags ) { HRESULT hRes = WBEM_S_NO_ERROR ; DWORD dwPropertiesReq = DFSJNPT_ALL_PROPS; hRes = EnumerateAllJnPts ( pMethodContext, dwPropertiesReq ); return hRes; } /***************************************************************************** * * FUNCTION : CDfsJnPt::GetObject * * DESCRIPTION : Find a single instance based on the key properties for the * class. * *****************************************************************************/ HRESULT CDfsJnPt :: GetObject ( CInstance *pInstance, long lFlags , CFrameworkQuery &Query ) { HRESULT hRes = WBEM_S_NO_ERROR; DWORD dwPropertiesReq = 0; CHString t_Key ; if ( pInstance->GetCHString ( DFSNAME , t_Key ) == FALSE ) { hRes = WBEM_E_INVALID_PARAMETER ; } if ( SUCCEEDED ( hRes ) ) { if ( Query.AllPropertiesAreRequired() ) { dwPropertiesReq = DFSJNPT_ALL_PROPS; } else { SetRequiredProperties ( Query, dwPropertiesReq ); } hRes = FindAndSetDfsEntry ( t_Key, dwPropertiesReq, pInstance, eGet ); } return hRes ; } /***************************************************************************** * * FUNCTION : CDfsJnPt::PutInstance * * DESCRIPTION : Adding a Instance if it already doesnt exist, or modify it * if it already exists, based on the kind of operation requested * *****************************************************************************/ HRESULT CDfsJnPt :: PutInstance ( const CInstance &Instance, long lFlags ) { HRESULT hRes = WBEM_E_FAILED ; CHString t_Key ; DWORD dwOperation; if ( Instance.GetCHString ( DFSNAME , t_Key ) ) { hRes = WBEM_S_NO_ERROR; DWORD dwPossibleOperations = 0; dwPossibleOperations = (WBEM_FLAG_CREATE_OR_UPDATE | WBEM_FLAG_UPDATE_ONLY | WBEM_FLAG_CREATE_ONLY); switch ( lFlags & dwPossibleOperations ) { case WBEM_FLAG_CREATE_OR_UPDATE: { dwOperation = eUpdate; break; } case WBEM_FLAG_UPDATE_ONLY: { dwOperation = eModify; break; } case WBEM_FLAG_CREATE_ONLY: { hRes = WBEM_E_INVALID_PARAMETER; break; } } } if ( SUCCEEDED ( hRes ) ) { // This call is made with 2nd parameter 0, indicating that it should not load an instance // with any parameter, it should simple search. hRes = FindAndSetDfsEntry ( t_Key, 0, NULL, eGet ); if ( SUCCEEDED ( hRes ) || ( hRes == WBEM_E_NOT_FOUND ) ) { switch ( dwOperation ) { case eModify: { if ( SUCCEEDED ( hRes ) ) { hRes = UpdateDfsJnPt ( Instance, eModify ); break; } } case eAdd: { hRes = WBEM_E_INVALID_PARAMETER; break; // Create not currently supported } case eUpdate: { if ( hRes == WBEM_E_NOT_FOUND ) { hRes = WBEM_E_INVALID_PARAMETER; // Create not currently supported } else { hRes = UpdateDfsJnPt ( Instance, eModify ); } break; } } } } return hRes ; } /***************************************************************************** * * FUNCTION : CDfsJnPt::CheckParameters * * DESCRIPTION : Performs the validity checks of the parameters to add/modify * the Dfs Jn Pts. * *****************************************************************************/ HRESULT CDfsJnPt :: CheckParameters ( const CInstance &a_Instance , int a_State ) { // Getall the Properties from the Instance to Verify bool t_Exists ; VARTYPE t_Type ; HRESULT hr = WBEM_S_NO_ERROR ; if ( a_State != WBEM_E_ALREADY_EXISTS ) { // need to validate the dfsEntryPath, if it already exists, means it was already verified and was in DFS tree and // hence need not verify if ( a_Instance.GetStatus ( DFSNAME , t_Exists , t_Type ) ) { if ( t_Exists && ( t_Type == VT_BSTR ) ) { CHString t_DfsEntryPath; if ( a_Instance.GetCHString ( DFSNAME , t_DfsEntryPath ) && ! t_DfsEntryPath.IsEmpty () ) { } else { // Zero Length string hr = WBEM_E_INVALID_PARAMETER ; } } else { hr = WBEM_E_INVALID_PARAMETER ; } } } if ( SUCCEEDED ( hr ) ) { if ( a_Instance.GetStatus ( STATE , t_Exists , t_Type ) ) { if ( t_Exists && ( t_Type == VT_I4 ) ) { DWORD dwState; if ( a_Instance.GetDWORD ( STATE, dwState ) ) { if (( dwState != 0 ) && ( dwState != 1 ) && ( dwState != 2 ) && ( dwState != 3 ) ) { hr = WBEM_E_INVALID_PARAMETER; } } } } } if ( SUCCEEDED ( hr ) ) { if ( a_Instance.GetStatus ( TIMEOUT , t_Exists , t_Type ) ) { if ( t_Exists ) { if ( t_Type != VT_I4 ) { hr = WBEM_E_INVALID_PARAMETER; } } } } return hr; } /***************************************************************************** * * FUNCTION : CDfsJnPt:: DeleteInstance * * DESCRIPTION : Deleting a Dfs Jn Pt if it exists * *****************************************************************************/ HRESULT CDfsJnPt :: DeleteInstance ( const CInstance &Instance, long lFlags ) { HRESULT hRes = WBEM_S_NO_ERROR ; CHString t_Key ; NET_API_STATUS t_Status = NERR_Success; if ( Instance.GetCHString ( DFSNAME , t_Key ) == FALSE ) { hRes = WBEM_E_INVALID_PARAMETER; } if ( SUCCEEDED ( hRes ) ) { hRes = FindAndSetDfsEntry ( t_Key, 0, NULL, eDelete ); } return hRes ; } /***************************************************************************** * * FUNCTION : CDfsJnPt::EnumerateAllJnPts * * DESCRIPTION : Enumerates all the Junction points and calls the method to load * Instance and then commit * ******************************************************************************/ HRESULT CDfsJnPt::EnumerateAllJnPts ( MethodContext *pMethodContext, DWORD dwPropertiesReq ) { HRESULT hRes = WBEM_S_NO_ERROR; PDFS_INFO_300 pData300 = NULL; DWORD er300 = 0; DWORD tr300 = 0; // Call the NetDfsEnum function, specifying level 300. DWORD res = NetDfsEnum( m_ComputerName.GetBuffer ( 0 ), 300, -1, (LPBYTE *) &pData300, &er300, &tr300 ); // If no error occurred, if(res==NERR_Success) { if ( pData300 != NULL ) { try { PDFS_INFO_300 p300 = pData300; CInstancePtr pInstance; for ( int i = 0; (i < er300) && SUCCEEDED( hRes ); i++, p300++ ) { DWORD er4 = 0; DWORD tr4 = 0; PDFS_INFO_4 pData4 = NULL; if (p300->DfsName != NULL) { if ( ( res = NetDfsEnum(p300->DfsName, 4, -1, (LPBYTE *) &pData4, &er4, &tr4) ) == NERR_Success) { if ( pData4 != NULL ) { try { PDFS_INFO_4 p4 = pData4; for ( int j = 0; (j < er4) && SUCCEEDED ( hRes ); j++, p4++ ) { pInstance.Attach(CreateNewInstance( pMethodContext )); hRes = LoadDfsJnPt ( dwPropertiesReq, pInstance, p4, p4 == pData4 ); if ( SUCCEEDED ( hRes ) ) { hRes = pInstance->Commit(); } } } catch(...) { NetApiBufferFree(pData4); pData4 = NULL; throw; } NetApiBufferFree(pData4); pData4 = NULL; } } // Check to see if there are ANY roots else if ( (res != ERROR_NO_MORE_ITEMS) && (res != ERROR_NO_SUCH_DOMAIN) && (res != ERROR_NOT_FOUND) && (res != ERROR_ACCESS_DENIED) ) { hRes = WBEM_E_FAILED; } } } } catch ( ... ) { NetApiBufferFree(pData300); pData300 = NULL; throw; } NetApiBufferFree(pData300); pData300 = NULL; } } else if ( (res != ERROR_NO_MORE_ITEMS) && (res != ERROR_NO_SUCH_DOMAIN) && (res != ERROR_NOT_FOUND) ) // Check to see if there are ANY roots { if ( ERROR_ACCESS_DENIED == res ) { hRes = WBEM_E_ACCESS_DENIED ; } else { hRes = WBEM_E_FAILED ; } } return hRes; } /***************************************************************************** * * FUNCTION : CDfsJnPt::FindAndSetDfsEntry * * DESCRIPTION : Finds an entry matching the dfsEntryPath and loads the * Instance if found or acts based on the Operation passed * ******************************************************************************/ HRESULT CDfsJnPt::FindAndSetDfsEntry ( LPCWSTR a_Key, DWORD dwPropertiesReq, CInstance *pInstance, DWORD eOperation ) { HRESULT hRes = WBEM_E_NOT_FOUND; PDFS_INFO_300 pData300 = NULL; DWORD er300 = 0; DWORD tr300 = 0; DWORD res = NERR_Success; // Call the NetDfsEnum function, specifying level 300. if( ( res = NetDfsEnum( m_ComputerName.GetBuffer ( 0 ), 300, -1, (LPBYTE *) &pData300, &er300, &tr300 ) ) == NERR_Success ) { if ( pData300 != NULL ) { try { BOOL bContinue = TRUE; PDFS_INFO_300 p300 = pData300; for ( int i = 0; (i < er300) && bContinue; i++, p300++ ) { if ( p300->DfsName != NULL ) { PDFS_INFO_4 pData4 = NULL; DWORD er4=0; DWORD tr4=0; if ( ( res = NetDfsEnum(p300->DfsName, 4, -1, (LPBYTE *) &pData4, &er4, &tr4) ) == NERR_Success ) { if ( pData4 != NULL ) { try { BOOL bFound = FALSE; PDFS_INFO_4 p4 = pData4; for ( int j = 0; (j < er4) && bContinue; j++, bContinue && (j < er4) ? p4++ : p4 ) { if ( _wcsicmp ( a_Key, p4->EntryPath ) == 0 ) { bFound = TRUE; bContinue = FALSE; } } if ( bFound ) { switch ( eOperation ) { case eGet : { hRes = LoadDfsJnPt ( dwPropertiesReq, pInstance, p4, p4 == pData4 ); break; } case eDelete: { hRes = DeleteDfsJnPt ( p4 ); break; } } } } catch(...) { NetApiBufferFree(pData4); pData4 = NULL; throw; } NetApiBufferFree(pData4); pData4 = NULL; } } // Check to see if there are ANY roots else if ( (res != ERROR_NO_MORE_ITEMS) && (res != ERROR_NO_SUCH_DOMAIN) && (res != ERROR_NOT_FOUND) && (res != ERROR_ACCESS_DENIED) ) { hRes = WBEM_E_FAILED; bContinue = FALSE; } } } } catch ( ... ) { NetApiBufferFree(pData300); pData300 = NULL; throw; } NetApiBufferFree(pData300); pData300 = NULL; } } else if ( (res != ERROR_NO_MORE_ITEMS) && (res != ERROR_NO_SUCH_DOMAIN) && (res != ERROR_NOT_FOUND) ) // Check to see if there are ANY roots { if ( ERROR_ACCESS_DENIED == res ) { hRes = WBEM_E_ACCESS_DENIED ; } else { hRes = WBEM_E_FAILED ; } } return hRes; } /***************************************************************************** * * FUNCTION : CDfsJnPt::LoadDfsJnPt * * DESCRIPTION : Loads a Dfs Junction point entry into the instance * ******************************************************************************/ HRESULT CDfsJnPt::LoadDfsJnPt ( DWORD dwPropertiesReq, CInstance *pInstance, PDFS_INFO_4 pJnPtBuf, bool bRoot ) { HRESULT hRes = WBEM_S_NO_ERROR; if (NULL != pInstance) { if ( dwPropertiesReq & DFSJNPT_PROP_DfsEntryPath ) { if ( pInstance->SetWCHARSplat ( DFSNAME, pJnPtBuf->EntryPath ) == FALSE ) { hRes = WBEM_E_FAILED; } } if ( pInstance->Setbool ( L"Root", bRoot ) == FALSE ) { hRes = WBEM_E_FAILED; } if ( dwPropertiesReq & DFSJNPT_PROP_State ) { // need to check the state and then valuemap DWORD dwState = 0; switch ( pJnPtBuf->State ) { case DFS_VOLUME_STATE_OK : { dwState = 0; break; } case DFS_VOLUME_STATE_INCONSISTENT : { dwState = 1; break; } case DFS_VOLUME_STATE_ONLINE : { dwState = 2; break; } case DFS_VOLUME_STATE_OFFLINE : { dwState = 3; break; } } if ( pInstance->SetDWORD ( STATE, dwState ) == FALSE ) { hRes = WBEM_E_FAILED; } } if ( dwPropertiesReq & DFSJNPT_PROP_Comment ) { if ( pInstance->SetWCHARSplat ( COMMENT, pJnPtBuf->Comment ) == FALSE ) { hRes = WBEM_E_FAILED; } } if ( dwPropertiesReq & DFSJNPT_PROP_Caption ) { if ( pInstance->SetWCHARSplat ( CAPTION, pJnPtBuf->Comment ) == FALSE ) { hRes = WBEM_E_FAILED; } } if ( dwPropertiesReq & DFSJNPT_PROP_Timeout ) { if ( pInstance->SetDWORD ( TIMEOUT, pJnPtBuf->Timeout ) == FALSE ) { hRes = WBEM_E_FAILED; } } } else { if (0 != dwPropertiesReq) { hRes = WBEM_E_FAILED; } } return hRes; } /***************************************************************************** * * FUNCTION : CDfsJnPt::DeleteDfsJnPt * * DESCRIPTION : Deletes a Junction Pt if it exists * ******************************************************************************/ HRESULT CDfsJnPt::DeleteDfsJnPt ( PDFS_INFO_4 pDfsJnPt ) { HRESULT hRes = WBEM_S_NO_ERROR; NET_API_STATUS t_Status = NERR_Success; if ( IsDfsRoot ( pDfsJnPt->EntryPath ) ) { if ((wcslen(pDfsJnPt->EntryPath) > 4) && (pDfsJnPt->EntryPath[0] == pDfsJnPt->EntryPath[1]) && (pDfsJnPt->EntryPath[0] == L'\\')) { wchar_t *pSlash = wcschr(&(pDfsJnPt->EntryPath[2]), L'\\'); if (pSlash > &(pDfsJnPt->EntryPath[2])) { wchar_t *pServer = new wchar_t[pSlash - &(pDfsJnPt->EntryPath[2]) + 1]; BOOL bRemove = FALSE; try { wcsncpy(pServer, &(pDfsJnPt->EntryPath[2]), pSlash - &(pDfsJnPt->EntryPath[2])); pServer[pSlash - &(pDfsJnPt->EntryPath[2])] = L'\0'; if (0 == m_ComputerName.CompareNoCase(pServer)) { bRemove = TRUE; } else { DWORD dwDnsName = 256; DWORD dwDnsNameSize = 256; wchar_t *pDnsName = new wchar_t[dwDnsName]; try { while (!ProviderGetComputerNameEx(ComputerNamePhysicalDnsHostname, pDnsName, &dwDnsName)) { if (GetLastError() != ERROR_MORE_DATA) { delete [] pDnsName; pDnsName = NULL; break; } else { delete [] pDnsName; pDnsName = NULL; dwDnsName = dwDnsNameSize * 2; dwDnsNameSize = dwDnsName; pDnsName = new wchar_t[dwDnsName]; } } } catch (...) { if (pDnsName) { delete [] pDnsName; pDnsName = NULL; } throw; } if (pDnsName) { if (_wcsicmp(pDnsName, pServer) == 0) { bRemove = TRUE; } delete [] pDnsName; pDnsName = NULL; } } } catch(...) { if (pServer) { delete [] pServer; } throw; } if (bRemove) { t_Status = NetDfsRemoveStdRoot ( pDfsJnPt->Storage->ServerName, pDfsJnPt->Storage->ShareName, 0 ); if ( t_Status != NERR_Success ) { hRes = WBEM_E_FAILED; } } else { //can't delete roots not on this machine hRes = WBEM_E_PROVIDER_NOT_CAPABLE; } delete [] pServer; } else { hRes = WBEM_E_FAILED; } } else { hRes = WBEM_E_FAILED; } } else { // Apparently there is no way to explicitly remove a link. However, if // you remove all the replicas, the link gets deleted automatically. for ( int StorageNo = 0; StorageNo < pDfsJnPt->NumberOfStorages; StorageNo++ ) { t_Status = NetDfsRemove ( pDfsJnPt->EntryPath, pDfsJnPt->Storage[StorageNo].ServerName, pDfsJnPt->Storage[StorageNo].ShareName ); if ( t_Status != NERR_Success ) { hRes = WBEM_E_FAILED; break; } } } return hRes; } /***************************************************************************** * * FUNCTION : CDfsJnPt::UpdateDfsJnPt * * DESCRIPTION : Adds / Modifies the Dfs Jn Pt * ******************************************************************************/ HRESULT CDfsJnPt::UpdateDfsJnPt ( const CInstance &Instance, DWORD dwOperation ) { HRESULT hRes = WBEM_S_NO_ERROR; NET_API_STATUS t_Status = NERR_Success; if ( SUCCEEDED ( hRes ) ) { NET_API_STATUS t_Status = NERR_Success; CHString t_EntryPath; Instance.GetCHString ( DFSNAME, t_EntryPath ); if ( dwOperation == eAdd ) { hRes = WBEM_E_INVALID_PARAMETER; } else if ( dwOperation == eModify ) { CHString t_Comment; if (( t_Status == NERR_Success ) && (Instance.GetCHString ( COMMENT, t_Comment ))) { DFS_INFO_100 t_dfsCommentData; t_dfsCommentData.Comment = t_Comment.GetBuffer( 0 ); t_Status = NetDfsSetInfo ( t_EntryPath.GetBuffer ( 0 ), NULL, NULL, 100, (LPBYTE) &t_dfsCommentData ); } if ( t_Status == NERR_Success ) { DFS_INFO_102 t_dfsTimeoutData; if (Instance.GetDWORD ( TIMEOUT, t_dfsTimeoutData.Timeout)) { t_Status = NetDfsSetInfo ( t_EntryPath.GetBuffer ( 0 ), NULL, NULL, 102, (LPBYTE) &t_dfsTimeoutData ); } } } if ((SUCCEEDED(hRes)) && ( t_Status != NERR_Success )) { hRes = WBEM_E_FAILED ; } } return hRes; } /***************************************************************************** * * FUNCTION : CDfsJnPt::AddDfsJnPt * * DESCRIPTION : Adds the New Dfs Jn Pt * ******************************************************************************/ NET_API_STATUS CDfsJnPt :: AddDfsJnPt ( LPWSTR a_DfsEntry, LPWSTR a_ServerName, LPWSTR a_ShareName, LPWSTR a_Comment ) { NET_API_STATUS t_Status = NERR_Success; wchar_t *t_slash = NULL; //simple analysis on the parameters... if ((a_ServerName == NULL) || (a_ShareName == NULL) || (a_ServerName[0] == L'\0') || (a_ShareName[0] == L'\0') || (a_DfsEntry == NULL) || (wcslen(a_DfsEntry) < 5) || (wcsncmp(a_DfsEntry, L"\\\\", 2) != 0)) { t_Status = ERROR_INVALID_PARAMETER; } else { t_slash = wcschr((const wchar_t*)(&(a_DfsEntry[2])), L'\\'); if ((t_slash == NULL) || (t_slash == &(a_DfsEntry[2]))) { t_Status = ERROR_INVALID_PARAMETER; } else { //let's find the next slash if there is one... t_slash++; if ((*t_slash == L'\0') || (*t_slash == L'\\')) { t_Status = ERROR_INVALID_PARAMETER; } else { //if t_slash is null we have a root t_slash = wcschr(t_slash, L'\\'); } } } if (t_Status == NERR_Success) { if ( t_slash ) { // this is a a junction point other than the root t_Status = NetDfsAdd ( a_DfsEntry, a_ServerName, a_ShareName, a_Comment, DFS_ADD_VOLUME ); } else { // it is DFSRoot DWORD dwErr = GetFileAttributes ( a_DfsEntry ); if ( dwErr != 0xffffffff ) { t_Status = NetDfsAddStdRoot ( a_ServerName, a_ShareName, a_Comment, 0 ); } else { t_Status = GetLastError(); } } } return t_Status; } /***************************************************************************** * * FUNCTION : CDfsJnPt::SetRequiredProperties * * DESCRIPTION : Sets the bitmap for the required properties * ******************************************************************************/ void CDfsJnPt::SetRequiredProperties ( CFrameworkQuery &Query, DWORD &dwPropertiesReq ) { dwPropertiesReq = 0; if ( Query.IsPropertyRequired ( DFSNAME ) ) { dwPropertiesReq |= DFSJNPT_PROP_DfsEntryPath; } if ( Query.IsPropertyRequired ( STATE ) ) { dwPropertiesReq |= DFSJNPT_PROP_State; } if ( Query.IsPropertyRequired ( COMMENT ) ) { dwPropertiesReq |= DFSJNPT_PROP_Comment; } if ( Query.IsPropertyRequired ( TIMEOUT ) ) { dwPropertiesReq |= DFSJNPT_PROP_Timeout; } } /***************************************************************************** * * FUNCTION : CDfsJnPt::DfsRoot * * DESCRIPTION : Checks if the Dfs Jn Pt is a root. * ******************************************************************************/ BOOL CDfsJnPt::IsDfsRoot ( LPCWSTR lpKey ) { BOOL bRetVal = TRUE; int i = 0; if ( lpKey [ i ] == L'\\' ) { i++; } if ( lpKey [ i ] == L'\\' ) { i++; } while ( lpKey [ i ] != L'\\' ) { i++; } i++; while ( lpKey [ i ] != L'\0' ) { if ( lpKey [ i ] == L'\\' ) { bRetVal = FALSE; break; } i++; } return bRetVal; } /***************************************************************************** * * FUNCTION : CDfsJnPt::ExecMethod * * DESCRIPTION : Executes a method * * INPUTS : Instance to execute against, method name, input parms instance * Output parms instance. * * OUTPUTS : none * * RETURNS : nothing * * COMMENTS : * *****************************************************************************/ HRESULT CDfsJnPt::ExecMethod ( const CInstance& a_Instance, const BSTR a_MethodName , CInstance *a_InParams , CInstance *a_OutParams , long a_Flags ) { HRESULT hr = WBEM_E_INVALID_METHOD; if (_wcsicmp(a_MethodName, L"Create") == 0) { CHString sDfsEntry, sServerName, sShareName, sDescription; if (a_InParams->GetCHString(DFSENTRYPATH, sDfsEntry) && sDfsEntry.GetLength() && a_InParams->GetCHString(SERVERNAME, sServerName) && sServerName.GetLength() && a_InParams->GetCHString(SHARENAME, sShareName) && sShareName.GetLength()) { // At the point, the *wmi* method call has succeeded. All that // remains is to determine the *class's* return code hr = WBEM_S_NO_ERROR; a_InParams->GetCHString(COMMENT, sDescription); NET_API_STATUS status = AddDfsJnPt ( sDfsEntry.GetBuffer(0), sServerName.GetBuffer(0), sShareName.GetBuffer(0), sDescription.GetLength() > 0 ? sDescription.GetBuffer(0) : NULL ); a_OutParams->SetDWORD(L"ReturnValue", status); } else { hr = WBEM_E_INVALID_METHOD_PARAMETERS; } } return hr; }