#include <windows.h>
#pragma hdrstop
#include <stddef.h>

#include "KbUnTar.h"

static const wchar_t g_cszMethodTar[] = L"TAR";
#ifdef  TGZ_SUPPORT
static const wchar_t g_cszMethodTgz[] = L"TGZ";
#endif //TGZ_SUPPORT

#define REGTYPE     '0' /* Regular file */
#define AREGTYPE    '\0'/* Regular file; old V7 format */
#define LNKTYPE     '1' /* Link */
#define SYMTYPE     '2' /* Symbolic link */
#define CHRTYPE     '3' /* Character special */
#define BLKTYPE     '4' /* Block special */
#define DIRTYPE     '5' /* Directory */
#define FIFOTYPE    '6' /* FIFO special */
#define CONTTYPE    '7' /* Continguous file */
#define LONGLINK    'L' /* Long name Link by tantan*/

#pragma pack(push, 1) //1バイト境界
struct TarHeader
{
    union{
        char dummy[512];
        struct{
            char name[100];
            char mode[8];
            char uid[8];
            char gid[8];
            char size[12];
            char mtime[12];
            char chksum[8];
            char typeflag;
            char linkname[100];
            char magic[6];
            char version[2];
            char uname[32];
            char gname[32];
            char devmajor[8];
            char devminor[8];
            /* Following fields were added by GNUtar */
            char atime[12];
            char ctime[12];
            char offset[12];
        };
    };
};
#pragma pack(pop)

static BOOL __fastcall IsUtf8(const char *cszStr, int nSrcSize)
{
    //(1)c2〜df+80〜bf　　　                           2バイト
    //(2)e0〜ef+80〜bf+80〜bf　　　                    3バイト
    //(3)f0〜f7+80〜bf +80〜bf +80〜bf                 4バイト
    //(4)f8〜fb+80〜bf +80〜bf +80〜bf +80〜bf         5バイト
    //(5)fc〜fd+80〜bf +80〜bf +80〜bf +80〜bf +80〜bf 6バイト
    DWORD p = 0;
    const DWORD dwSize = nSrcSize;
    const BYTE *c = (const BYTE*)cszStr;
    while (p < dwSize && c[p]) {
        if (c[p] <= 0x7f) {
            p++;
            continue;
        }
        if (0xc2 <= c[p] && c[p] <= 0xdf) {
            //(1)c2〜df+80〜bf　　　                           2バイト
            if (p + 1 >= dwSize) {
                return false;
            }
            if (0x80 <= c[p + 1] && c[p + 1] <= 0xbf) {
                p += 2;
                continue;
            }
        }
        else if (0xe0 <= c[p] && c[p] <= 0xef) {//(2)
        //(2)e0〜ef+80〜bf+80〜bf　　　                    3バイト
            if (p + 2 >= dwSize) {
                return false;
            }
            if (0x80 <= c[p + 1] && c[p + 1] <= 0xbf &&
                0x80 <= c[p + 2] && c[p + 2] <= 0xbf) {
                p += 3;
                continue;
            }
        }
        else if (0xf0 <= c[p] && c[p] <= 0xf7) {
            //(3)f0〜f7+80〜bf +80〜bf +80〜bf                 4バイト
            if (p + 3 >= dwSize) {
                return false;
            }
            if (0x80 <= c[p + 1] && c[p + 1] <= 0xbf &&
                0x80 <= c[p + 2] && c[p + 2] <= 0xbf &&
                0x80 <= c[p + 3] && c[p + 3] <= 0xbf) {
                p += 4;
                continue;
            }
        }
        else if (0xf8 <= c[p] && c[p] <= 0xfb) {//(4)
        //(4)f8〜fb+80〜bf +80〜bf +80〜bf +80〜bf         5バイト
            if (p + 4 >= dwSize) {
                return false;
            }
            if (0x80 <= c[p + 1] && c[p + 1] <= 0xbf &&
                0x80 <= c[p + 2] && c[p + 2] <= 0xbf &&
                0x80 <= c[p + 3] && c[p + 3] <= 0xbf &&
                0x80 <= c[p + 4] && c[p + 4] <= 0xbf) {
                p += 5;
                continue;
            }
        }
        else if (0xfc <= c[p] && c[p] <= 0xfd) {
            //(5)fc〜fd+80〜bf +80〜bf +80〜bf +80〜bf +80〜bf 6バイト
            if (p + 5 >= dwSize) {
                return false;
            }
            if (0x80 <= c[p + 1] && c[p + 1] <= 0xbf &&
                0x80 <= c[p + 2] && c[p + 2] <= 0xbf &&
                0x80 <= c[p + 3] && c[p + 3] <= 0xbf &&
                0x80 <= c[p + 4] && c[p + 4] <= 0xbf &&
                0x80 <= c[p + 5] && c[p + 5] <= 0xbf) {
                p += 6;
                continue;
            }
        }
        return false;
    }
    return true;
}

static DWORD __fastcall TarStrToDWORD(const char* s, int len)
{//8 進文字列を DWORD に変換
    int i;
    DWORD ret = 0;
    for(i = 0;  i < len; i++ ){
        if('0' <= s[i] && s[i] <= '7'){
            ret = (ret << 3) + (s[i] - '0');
        }
        else if(s[i] != ' '){
            break;
        }
    }
    return ret;
}
static BOOL __fastcall TarCheckSum(const TarHeader* pTarHeader)
{//チェックサムが正しければ TRUE を返す
    const BYTE* dat= (const BYTE*)pTarHeader;
    DWORD dwCheckSum = 0;
    DWORD i = 0;
    while(i < 148){
        dwCheckSum += dat[i++];
    }
    while(i < 156){//pTarHeader->chksum
        dwCheckSum += ' ';
        i++;
    }
    while(i < 512){
        dwCheckSum += dat[i++];
    }
    return dwCheckSum == TarStrToDWORD(pTarHeader->chksum, sizeof(pTarHeader->chksum));
}
////////////////////////////////////////////////////////////////////////////////
KbUnTar::KbUnTar(UnArcConfig *pConfig)
    : KbUnArc(pConfig)
{
    m_pFile = NULL;
    m_dwCount = 0;
    m_dwCapacity = 0;
    m_ppTarInfo = NULL;
    m_bIsGZ = FALSE;
    m_hHeap = HeapCreate(0, 32768, 0);
}
////////////////////////////////////////////////////////////////////////////////
KbUnTar::~KbUnTar(void)
{
    if(m_pFile){
        m_pFile->Release();
    }
    HeapDestroy(m_hHeap);
}
////////////////////////////////////////////////////////////////////////////////
BOOL  __fastcall KbUnTar::Open(IKpiFile *pFile, IKpiFolder *pFolder, const wchar_t *cszOutputPath)
{
    DWORD dwSolid = 0;
    const KPI_FILEINFO *pFileInfo;
    m_pFile = pFile;
    pFile->AddRef();
#ifdef TGZ_SUPPORT
    if(EnumFileInfo(0, &pFileInfo)){
        m_ArchiveInfo.cb = sizeof(m_ArchiveInfo);
        m_ArchiveInfo.dwCount = -1;
        m_ArchiveInfo.dwSolid = 0;
        return TRUE;
    }
    //先頭の 2 バイトから gzip かどうか判別
    pFile->Seek(0, FILE_BEGIN);
    const WORD gz_magic = 0x8b1f;
    WORD magic = 0;
    pFile->Read(&magic, sizeof(magic));
    if(magic != gz_magic){
        pFile->Release();//gzip ではない
        m_pFile = NULL;
        return FALSE;
    }
    pFile->Seek(0, FILE_BEGIN);
    KbGzFile *pGzFile = new KbGzFile;
    if(!pGzFile->Open(pFile)){
        pGzFile->Release();
        pFile->Release();
        m_pFile = NULL;
        return FALSE;
    }
    pFile->Release();
    m_pFile = pFile = pGzFile;
    dwSolid = 1;//TGZ はソリッド書庫と見なす    
#endif
    m_bIsGZ = TRUE;
    if(!EnumFileInfo(0, &pFileInfo)){
        pFile->Release();
        m_pFile = NULL;
        return FALSE;
    }
    m_ArchiveInfo.cb = sizeof(m_ArchiveInfo);
    m_ArchiveInfo.dwCount = -1;
    m_ArchiveInfo.dwSolid = dwSolid;
    return TRUE;
}
BOOL WINAPI KbUnTar::GetArchiveInfo(const KPI_ARCHIVEINFO **ppArchiveInfo)
{
    *ppArchiveInfo = &m_ArchiveInfo;
    return TRUE;
}
BOOL WINAPI KbUnTar::Extract(DWORD dwIndex, IKpiFile **ppFile, UINT64 qwSize)
{
    *ppFile = NULL;
    const KPI_FILEINFO *pFileInfo;
    if(!EnumFileInfo(dwIndex, &pFileInfo)){
        return FALSE;
    }
    const TarInfo *pTarInfo = (TarInfo*)pFileInfo;//ちょっと危険？
    if(qwSize > pTarInfo->fi.qwOriginalSize){
        qwSize = pTarInfo->fi.qwOriginalSize;
    }
    UINT64 qwStartOffset = pTarInfo->qwStartOffset;
    UINT64 qwEndOffset = qwStartOffset + qwSize;
    IKpiFile *pFile = NULL;
    if(m_pFile->CreateClone(&pFile)){//m_pFile をそのまま使ってはいけない
    //Extract が返す IKpiFile は CreateClone 等に対応する必要はない。
        *ppFile = new KbKpiPartialFile(pFile,
                                       qwStartOffset,
                                       qwEndOffset,
                                       pTarInfo->pszFileName);
        pFile->Release();
        return TRUE;
    }
    return FALSE;
}
BOOL WINAPI KbUnTar::EnumFileInfo(DWORD dwIndex, const KPI_FILEINFO **ppFileInfo)
{//Open 時に一気に列挙しないで必要に応じて列挙する。
 //TGZ の列挙はかなり低速。
    if(dwIndex < m_dwCount){
        *ppFileInfo = &m_ppTarInfo[dwIndex]->fi;
        return TRUE;
    }
    IKpiFile *pFile = m_pFile;
    UINT64 qwOffset = pFile->Seek(0, FILE_CURRENT);//現在のファイルポインタ
    DWORD  dwSize;                //格納ファイルのサイズ
    DWORD  dwFnameLen;            //格納ファイル名の長さ
    UINT64 qwStartOffset;       //格納ファイルの開始位置
    char   szFileName[MAX_PATH*2];//格納ファイル名
    do{
        TarHeader header;
        if(pFile->Read(&header, sizeof(header)) < sizeof(header)){
            break;
        }
        if(!TarCheckSum(&header)){
            break;
        }
        qwOffset += sizeof(header);

        dwSize = TarStrToDWORD(header.size, sizeof(header.size));
        qwStartOffset = qwOffset;
        if(header.typeflag == LONGLINK){//ファイル名の長さが 100 バイトを超える
            dwFnameLen = dwSize;
            if(dwFnameLen >= sizeof(szFileName)){
                dwFnameLen = 0;//ファイル名が長すぎる場合は無視する。
            }
            if(pFile->Read(szFileName, dwFnameLen) < dwFnameLen){
                break;
            }
            szFileName[dwFnameLen] = 0;

            qwOffset += ((dwSize + 511) & ~511);
            pFile->Seek(qwOffset, FILE_BEGIN);
            if(pFile->Read(&header, sizeof(header)) < sizeof(header)){
                break;
            }
            if(!TarCheckSum(&header)){
                break;
            }
            qwOffset += sizeof(header);
            qwStartOffset = qwOffset;
            dwSize = TarStrToDWORD(header.size, sizeof(header.size));
        }
        else if(header.typeflag == REGTYPE || //通常のファイル
                header.typeflag == AREGTYPE ||//通常のファイル(V7フォーマット)
                header.typeflag == DIRTYPE){  //ディレクトリ
            //ファイル名の長さが 100 バイト以下
            dwFnameLen = 0;
            while(header.name[dwFnameLen]){
                if(dwFnameLen == sizeof(header.name)){
                    break;
                }
                dwFnameLen++;
            }
            memcpy(szFileName, header.name, dwFnameLen);
            szFileName[dwFnameLen] = 0;
        }
        else{
            qwOffset += ((dwSize + 511) & ~511);
            pFile->Seek(qwOffset, FILE_BEGIN);
            continue;
        }
        qwOffset += ((dwSize + 511) & ~511);
        pFile->Seek(qwOffset, FILE_BEGIN);
        if(dwFnameLen == 0){//ファイル名が無効な場合はスキップ
            continue;
        }
        DWORD dwUtf16FnameLen;
        //ファイル名が UTF-8 で格納されてる場合とそうでない場合
        //がある
        BOOL bIsUtf8 = IsUtf8(szFileName, dwFnameLen);//判別処理
        if(bIsUtf8){//utf8
            dwUtf16FnameLen = MultiByteToWideChar(CP_UTF8, 0, szFileName, dwFnameLen, NULL, 0);
        }
        else{
            dwUtf16FnameLen = MultiByteToWideChar(CP_ACP, 0, szFileName, dwFnameLen, NULL, 0);
        }
        TarInfo *pTarInfo = (TarInfo*)HeapAlloc(m_hHeap, 
                                                HEAP_ZERO_MEMORY, 
                                                sizeof(TarInfo) + (dwUtf16FnameLen+1)*sizeof(wchar_t));
        pTarInfo->qwStartOffset = qwStartOffset;
        if(bIsUtf8){//utf8
            MultiByteToWideChar(CP_UTF8, 0, szFileName, dwFnameLen, 
                                pTarInfo->pszFileName, dwUtf16FnameLen);
            pTarInfo->pszFileName[dwUtf16FnameLen] = 0;
        }
        else{
            MultiByteToWideChar(CP_ACP, 0, szFileName, dwFnameLen, 
                                pTarInfo->pszFileName, dwUtf16FnameLen);
            pTarInfo->pszFileName[dwUtf16FnameLen] = 0;
        }
        kbBSlashToSlash(pTarInfo->pszFileName);
        //KPI_FILEINFO
        pTarInfo->fi.cb = sizeof(pTarInfo->fi);
        pTarInfo->fi.dwType = 0;
        pTarInfo->fi.cszFileName = pTarInfo->pszFileName;
        pTarInfo->fi.cszMethod = g_cszMethodTar;
        pTarInfo->fi.bCrypted = FALSE;
        pTarInfo->fi.bStored = TRUE;
#ifdef TGZ_SUPPORT
        if(m_bIsGZ){
            pTarInfo->fi.cszMethod = g_cszMethodTgz;
            pTarInfo->fi.bStored = FALSE;
        }
#endif
        pTarInfo->fi.qwOriginalSize = dwSize;
        pTarInfo->fi.qwCompressedSize = dwSize;
        //http://homepage1.nifty.com/dangan/Content/Program/Java/jLHA/Notes/DataNotes.html
        //FILETIME を 64bit値とみなした場合の time_t への 変換式は
        //time_t = ( FILETIME - 0x19DB1DED53E8000 ) / 10000000;
        //となる。(閏秒、ユリウス暦からグレゴリオ暦への移行等は考慮しない) 
        //FILETIME = (time_t * 10000000) + 0x19DB1DED53E8000;
        union{
            FILETIME ft;
            UINT64   qwft;
        };
        DWORD dwDate = TarStrToDWORD(header.mtime, sizeof(header.mtime));
        qwft = (((UINT64)dwDate) * 10000000) + 0x19DB1DED53E8000;
        FileTimeToLocalFileTime(&ft, &pTarInfo->fi.ft);

        if(m_dwCount >= m_dwCapacity){
            m_dwCapacity += 32;
            if(m_ppTarInfo){
                m_ppTarInfo = (TarInfo**)HeapReAlloc(m_hHeap, 
                                                     0, 
                                                     m_ppTarInfo, 
                                                     sizeof(TarInfo*)*m_dwCapacity);
            }
            else{
                m_ppTarInfo = (TarInfo**)HeapAlloc(m_hHeap, 
                                                   0, 
                                                   sizeof(TarInfo*)*m_dwCapacity);
            }
            //この部分を 
            //pTarInfo* へのポインタの配列としてではなく TarInfo の配列として実装して、
            //
            //m_pTarInfo = (TarInfo*)HeapReAlloc(m_hHeap, 
            //                                   0, 
            //                                   m_pTarInfo, 
            //                                   sizeof(TarInfo)*m_dwCapacity);
            //...
            //*ppFileInfo = &m_pTarInfo[m_dwCount].fi;
            //のようにすると、前回呼び出し時に返した pFileInfo が無効になることがあり、
            //再現性の低いバグに悩まされることになる。
        }
        m_ppTarInfo[m_dwCount] = pTarInfo;
        if(dwIndex == m_dwCount++){
            *ppFileInfo = &pTarInfo->fi;
            return TRUE;
        }
    }while(1);
    *ppFileInfo = NULL;
    return FALSE;
}
