#include "FileHash.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>


#define PARTITIAL_LIMIT (1)		// (단위:MByte))
typedef unsigned long long UINT64;


CFileHash::CFileHash()
{
    MDInit( &m_contextForBuffer );
    m_nPartitialSize = PARTITIAL_LIMIT*(1024*1024);
}

string CFileHash::GetLastErrorMessage()
{
    string szMessage = m_szLastErrorMessage;
    return szMessage;
}

int CFileHash::GetPartitialSize()
{
    return m_nPartitialSize/(1024*1024);
}

bool CFileHash::SetPartitialSize( int nPartitialSize )
{
    if( nPartitialSize <= 0)
        return false;
    m_nPartitialSize = nPartitialSize*(1024*1024);
    return true;
}

void CFileHash::UpdateBufferHash(unsigned char* szBuffer, int nLen )
{
    MDUpdate( &m_contextForBuffer, szBuffer, nLen );
}

void CFileHash::GetBufferHash(string& szResult)
{
    unsigned char szDigest[16];
    char szHexBuffer[3];
	int i;

	szResult.clear();
	// 멤버 context를 이해애서 Digest값을 얻어온다.
    MDFinal( szDigest, &m_contextForBuffer );
	// 해시값을 hex_digest 값으로 바꿔준다.
	for( i = 0; i<16 ; i++ )
	{
		memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
		sprintf( szHexBuffer, "%02x", szDigest[i] );
		szResult.append( (char*)szHexBuffer);
	}
}

/// @breif Buffer Hash 했던 값을 초기화 하는 함수.
void CFileHash::ClearBufferHash()
{
    MDInit( &m_contextForBuffer );
}

/// @brief Sender에서 사용하는 함수로서 파일에 대한 해쉬값을 레벨에 따라 생성한다.
/// @param szFileFullPath [in] 파일의 절대경로 값.
/// @param nHashLevel [in] 해싱 방법에 대한 레벨 값.
/// @param szResult [out] 해싱된 결과값을 되돌려주는 변수.
/// @return 성공은 return true, 그 외에는 return false
bool CFileHash::GetFileHash(const string& szFileFullPath, int nHashLevel, string& szResult)
{
    struct stat filestatus;
                
    switch(nHashLevel)
    {
        case HASH_LEVEL_NONE:
            szResult = "0000";
            break;
        case HASH_LEVEL_FULL:
            // 해시 대상파일 오픈.
            if( !GetHashFull(szFileFullPath, szResult) )
            {
                return false;
            }
            break;
        case HASH_LEVEL_PARTITIAL:
            // 파일 사이즈를 체크한다. PARTITIAL_LIMIT 사이즈 이하이면 
            stat( szFileFullPath.c_str(), &filestatus );
			if( filestatus.st_size <= m_nPartitialSize*2 )
            {
                // 2MB이하일경우 파일전체 해시값을 구한다.
                if( !GetHashFull(szFileFullPath, szResult) )
                {
                    return false;
                }                
            }else 
            {
                // 옵션일 경우 PARTITIAL_LIMIT사이즈의 1/2을 
                // 파일 맨앞과 맨뒤에서 추출해서 해시값을 생성한다.
                if( !GetHashPartitial(szFileFullPath, szResult) )
                {
                    return false;
                }            
            }
            break;

		case HASH_LEVEL_FRONT_PARTITIAL:
			// 파일 사이즈를 체크한다. 설정된 값 이하이면 
			stat( szFileFullPath.c_str(), &filestatus );
			if( filestatus.st_size <= m_nPartitialSize )
			{
				// 파일전체 해시값을 구한다.
				if( !GetHashFull(szFileFullPath, szResult) )
				{
					return false;
				}                
			}
			else 
			{
				// 파일 맨앞에서 해시값을 생성한다.
				if( !GetHashFrontPartitial(szFileFullPath, szResult) )
				{
					return false;
				}            
			}
			break;


        default:
            sprintf( m_szLastErrorMessage, "Not support level[%d] on this class", nHashLevel);
            return false;
        break;
    }
    
    return true;
}

/// @brief Receiver에서 사용하는 함수로서 해당파일의 해시값과 Sender에서 받은 해싱값을 비교한 결과를 전달한다.
/// @param szFileFullPath [in] 파일의 절대경로 값.
/// @param nHashLevel [in] 해싱 방법에 대한 레벨 값.
/// @param szResult [out] Sender로 부터 받은 해시 값.
/// @return 같은 값일경우 return true, 그 외에는 return false
bool CFileHash::GetHashFull(const string& szFileFullPath, string& szResult)
{
    int fd;
    int nLen;
	int i;
    MD_CTX context;
    unsigned char szBuffer[1024], szDigest[16];
	char szHexBuffer[3];
    
	memset(szBuffer, 0x00, sizeof(szBuffer));
	memset(szDigest, 0x00, sizeof(szDigest));
	memset(szHexBuffer, 0x00, sizeof(szHexBuffer));
	szResult.clear();
    
    if( ( fd = open(szFileFullPath.c_str(), O_RDONLY ) ) == -1 )
	{
        sprintf( m_szLastErrorMessage, "File open failed.[%s] ", szFileFullPath.c_str());
        return false;
	}
    else
	{
        MDInit( &context );

        while( ( nLen = read(fd, szBuffer, 1024) ) )
            MDUpdate( &context, szBuffer, nLen );
        
		MDFinal( szDigest, &context );
		
		// 해시값을 hex_digest 값으로 바꿔준다.
		for( i = 0; i<16 ; i++ )
		{
			memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
			sprintf( szHexBuffer, "%02x", szDigest[i] );
			szResult.append( (char*)szHexBuffer);
		}
        close( fd ); 
	}
	return true;
}

bool CFileHash::GetHashPartitial(const string &szFileFullPath, string& szResult)
{
    int fd;
    MD_CTX context;
    int nLen;
	int i;
    unsigned char szBuffer[1024], szDigest[16];
	char szHexBuffer[3];
    UINT64 nFilePos;
    
	memset(szBuffer, 0x00, sizeof(szBuffer));
	memset(szDigest, 0x00, sizeof(szDigest));
	memset(szHexBuffer, 0x00, sizeof(szHexBuffer));
	szResult.clear();
    
    if( ( fd = open(szFileFullPath.c_str(), O_RDONLY ) ) == -1 )
	{
        sprintf( m_szLastErrorMessage, "File open failed.[%s] ", szFileFullPath.c_str());
        return false;
	}
    else
	{
        MDInit( &context );
        // 파일의 앞부분의 해시값을 추출한다.
        nFilePos = 0;
        
		while( (nLen = read(fd, szBuffer, 1024)) )
        {
            MDUpdate( &context, szBuffer, nLen );
            nFilePos = lseek(fd, 0, SEEK_CUR);
            // 앞부분의 해시할 값까지만 하기위한 조건.
            if( (nFilePos >= (UINT64)(m_nPartitialSize)) )
                break;
		}
        
        // 파일의 끝에서 해시값 추출할 시작점으로 파일포지션을 옮겨준다.
		lseek(fd, 0, SEEK_END);
        lseek(fd, -m_nPartitialSize , SEEK_END);

        while( (nLen = read(fd, szBuffer, 1024)) )
        {
            MDUpdate( &context, szBuffer, nLen );
            nFilePos = lseek(fd, 0, SEEK_CUR);
        }
        MDFinal( szDigest, &context );
		
		// 해시값을 hex_digest 값으로 바꿔준다.
		for( i = 0; i<16 ; i++ )
		{
			memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
			sprintf( szHexBuffer, "%02x", szDigest[i] );
			szResult.append( (char*)szHexBuffer);
		}
        close( fd );
	}
	return true;
}

bool CFileHash::GetHashFrontPartitial(const string &szFileFullPath, string& szResult)
{
    int fd;
    MD_CTX context;
    int nLen;
	int i;
    unsigned char szBuffer[1024], szDigest[16];
	char szHexBuffer[3];
    UINT64 nFilePos;
    
	memset(szBuffer, 0x00, sizeof(szBuffer));
	memset(szDigest, 0x00, sizeof(szDigest));
	memset(szHexBuffer, 0x00, sizeof(szHexBuffer));
	szResult.clear();
    
    if( ( fd = open(szFileFullPath.c_str(), O_RDONLY ) ) == -1 )
	{
        sprintf( m_szLastErrorMessage, "File open failed.[%s] ", szFileFullPath.c_str());
        return false;
	}
    else
	{
        MDInit( &context );
        // 파일의 앞부분의 해시값을 추출한다.
        nFilePos = 0;
        
		while( (nLen = read(fd, szBuffer, 1024)) )
        {
            MDUpdate( &context, szBuffer, nLen );
            nFilePos = lseek(fd, 0, SEEK_CUR);
            // 앞부분의 해시할 값까지만 하기위한 조건.
            if( (nFilePos >= (UINT64)(m_nPartitialSize)) )
                break;
		}
        
        MDFinal( szDigest, &context );
		
		// 해시값을 hex_digest 값으로 바꿔준다.
		for( i = 0; i<16 ; i++ )
		{
			memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
			sprintf( szHexBuffer, "%02x", szDigest[i] );
			szResult.append( (char*)szHexBuffer);
		}
        close( fd );
	}
	return true;
}

bool CFileHash::CheckFileHash(const string& szFileFullPath, int nHashLevel, const string szCompareHash)
{
    string szMd5;

    if( !GetFileHash(szFileFullPath, nHashLevel, szMd5 ) )
    {
        return false;
    }
    
    if( szCompareHash.compare(szMd5) != 0 )
    {
        sprintf( m_szLastErrorMessage, "Hash not match. source[%s] local[%s]", szCompareHash.c_str(), szMd5.c_str());
        return false;
    }
    return true;
}





// NEW 2016-05-04 huibong 토토디스크 지원용 기능 추가 (#27460, #27533)
/// @brief 토토디스크 고객사 지원용 파일 Hash 정보 추출 함수
/// @param szFileFullPath [in] 파일의 절대경로 값.
/// @param nFileTotalSize [in] 파일 크기 값.
/// @param szResultTotosoraMd5 [out] TOTOROSA MD5 추출 결과값을 반환하기 위한 변수.
/// @param szResultDnaCheckKey [out] 토토 DNA Check Key 추출 결과값을 반환하기 위한 변수.
/// @return 성공은 return true, 그 외에는 return false
bool CFileHash::GetHashTOTO( const string &szFileFullPath, const unsigned long long nFileTotalSize, string& szResultTotosoraMd5, string& szResultDnaCheckKey )
{			
	int fd;
	MD_CTX context;
	
	int i;
	unsigned char szBuffer[10240], szDigest[16];  
	char szHexBuffer[3];
	
	memset( szBuffer, 0x00, sizeof( szBuffer ) );
	memset( szDigest, 0x00, sizeof( szDigest ) );
	memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
	
	szResultTotosoraMd5.clear();
	szResultDnaCheckKey.clear();

	if( (fd = open( szFileFullPath.c_str(), O_RDONLY )) == -1 )
	{
		sprintf( m_szLastErrorMessage, "File open failed. [%s] ", szFileFullPath.c_str() );
		return false;
	}
	else
	{
		////////////// TOTOROSA MD5 해시 추출 Start ///////////////////////
		// 2016-05-04 huibong ( #27460 )
		// 토토디스크 고객사 지원을 위해 
		// 토토디스크에서 사용하는 Content Hash 추출 방법에 따른 기능 개발
		// 추출 관련해서는 MD5 Hash 추출 방법을 사용하며...
		// 파일 앞 최대 약 1MByte 까지 추출하고...  파일 크기가 1 M 이상이면.. 맨뒤의 10 KByte 추가 추출
		// 하지만.. 고객사 Java 코드 보니까.. 추출 코드가 전반적으로 이상하게 되어 있어....
		// 임의 수정시 문제 될 수 있으므로... 고객사에서 사용한 추출 Flow 를 그대로 사용한다.
		

		// 관련 변수 초기화.
		long nFileSize = 0;
		int dwBytesRead = 0;
		
		bool bRun = true;

		// MD5 hash 객체 초기화.
		MDInit( &context );

		// 최초 10,240 만큼 read.
		dwBytesRead = read( fd, szBuffer, 10240 );

		while( dwBytesRead > 0 )
		{
			nFileSize += dwBytesRead;
			MDUpdate( &context, szBuffer, dwBytesRead );

			if( nFileSize > 1048576 )
			{
				if( (nFileTotalSize > 1048576 + 10240) && bRun )
				{
					// 파일의 끝에서 해시값 추출할 시작점으로 파일포지션을 옮겨준다.
					lseek( fd, 0, SEEK_END );
					lseek( fd, -10240, SEEK_END );
					
					bRun = false;
				}
				else 
				{
					break;
				}
			}

			dwBytesRead = read( fd, szBuffer, 10240 );
		}

		MDFinal( szDigest, &context );
		
		// 해시값을 hex_digest 값으로 바꿔준다.
		for( i = 0; i<16; i++ )
		{
			memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
			sprintf( szHexBuffer, "%02x", szDigest[i] );
			szResultTotosoraMd5.append( (char*)szHexBuffer );
		}		
		////////////// TOTOROSA MD5 해시 추출 End ///////////////////////
		

		// 파일 Position 을 맨 앞으로 이동 처리한다.
		lseek( fd, 0, SEEK_SET );

		// 사용 버퍼 객체 초기화.
		memset( szBuffer, 0x00, sizeof( szBuffer ) );
		memset( szDigest, 0x00, sizeof( szDigest ) );
		memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );


		
		////////////// DNA CHECK Key 추출 Start ///////////////////////
		// 2016-05-10 huibong ( #27533 )
		// 토토디스크 고객사 지원을 위해 토토디스크에서 사용하는 Content DNA 추출 방법에 따른 기능 개발
		// 추출 관련해서는 MD5 Hash 추출 방법을 사용하며...
		// 파일 앞 최대 약 10MByte 까지 추출한 후 Hash 생성 후.. 마지막에 파일 Size 정보를 append 처리하는 방식 사용
		// 하지만.. 고객사 Java 코드 보니까..  코드가 전반적으로 이상하게 되어 있어만...
		// 임의 수정시 문제 될 수 있으므로... 고객사에서 사용한 추출 Flow 를 그대로 사용한다.
		
		// MD5 hash 객체 초기화.
		MDInit( &context );
		
		nFileSize = 10485760; // 10MB
		if( nFileTotalSize <= 10485760 )
		{
			nFileSize = nFileTotalSize;
		}
				
		do 
		{
			if( nFileSize < 1024 ) 
			{
				dwBytesRead = read( fd, szBuffer, (int)nFileSize );
			}
			else 
			{
				dwBytesRead = read( fd, szBuffer, 1024 );				
			}

			MDUpdate( &context, szBuffer, dwBytesRead );
			nFileSize = nFileSize - dwBytesRead;
						
		} while( nFileSize > 0 );

		MDFinal( szDigest, &context );

		// 해시값을 hex_digest 값으로 바꿔준다.
		for( i = 0; i<16; i++ )
		{
			memset( szHexBuffer, 0x00, sizeof( szHexBuffer ) );
			sprintf( szHexBuffer, "%02x", szDigest[i] );
			szResultDnaCheckKey.append( (char*)szHexBuffer );
		}

		// 추출된 10M Hash 결과에 파일 Size 정보를 append 처리한다.
		memset( szBuffer, 0x00, sizeof( szBuffer ) );
		sprintf( (char*)szBuffer, "%llu", nFileTotalSize );

		szResultDnaCheckKey.append( (char *)szBuffer );
		

		////////////// DNA CHECK Key 추출 End ///////////////////////
		
		
		// 파일 close
		close( fd );
	}
	

	return true;
}

