// CpioHandler.cpp

#include "StdAfx.h"

#include "../../../C/CpuArch.h"

#include "../../Common/ComTry.h"
#include "../../Common/MyLinux.h"
#include "../../Common/StringConvert.h"
#include "../../Common/StringToInt.h"
#include "../../Common/UTFConvert.h"

#include "../../Windows/PropVariant.h"
#include "../../Windows/TimeUtils.h"

#include "../Common/LimitedStreams.h"
#include "../Common/ProgressUtils.h"
#include "../Common/RegisterArc.h"
#include "../Common/StreamUtils.h"

#include "../Compress/CopyCoder.h"

#include "Common/ItemNameUtils.h"

using namespace NWindows;

namespace NArchive {
namespace NCpio {

static const Byte kMagicBin0 = 0xC7;
static const Byte kMagicBin1 = 0x71;

// #define MAGIC_ASCII { '0', '7', '0', '7', '0' }

static const Byte kMagicHex    = '1'; // New ASCII Format
static const Byte kMagicHexCrc = '2'; // New CRC Format
static const Byte kMagicOct    = '7'; // Portable ASCII Format

static const char *kName_TRAILER = "TRAILER!!!";

static const unsigned k_BinRecord_Size = 2 + 8 * 2 + 2 * 4;
static const unsigned k_OctRecord_Size = 6 + 8 * 6 + 2 * 11;
static const unsigned k_HexRecord_Size = 6 + 13 * 8;

static const unsigned k_RecordSize_Max = k_HexRecord_Size;

  /*
  struct CBinRecord
  {
    unsigned short c_magic;
    short c_dev;
    unsigned short c_ino;
    unsigned short c_mode;
    unsigned short c_uid;
    unsigned short c_gid;
    unsigned short c_nlink;
    short c_rdev;
    unsigned short c_mtimes[2];
    unsigned short c_namesize;
    unsigned short c_filesizes[2];
  };

  struct CHexRecord
  {
    char Magic[6];
    char inode[8];
    char Mode[8];
    char UID[8];
    char GID[8];
    char nlink[8];
    char mtime[8];
    char Size[8]; // must be 0 for FIFOs and directories
    char DevMajor[8];
    char DevMinor[8];
    char RDevMajor[8];  //only valid for chr and blk special files
    char RDevMinor[8];  //only valid for chr and blk special files
    char NameSize[8]; // count includes terminating NUL in pathname
    char ChkSum[8];  // 0 for "new" portable format; for CRC format the sum of all the bytes in the file
  };
*/

enum EType
{
  k_Type_BinLe,
  k_Type_BinBe,
  k_Type_Oct,
  k_Type_Hex,
  k_Type_HexCrc
};

static const char * const k_Types[] =
{
    "Binary LE"
  , "Binary BE"
  , "Portable ASCII"
  , "New ASCII"
  , "New CRC"
};

struct CItem
{
  AString Name;
  UInt32 inode;
  UInt32 Mode;
  UInt32 UID;
  UInt32 GID;
  UInt64 Size;
  UInt32 MTime;

  UInt32 NumLinks;
  UInt32 DevMajor;
  UInt32 DevMinor;
  UInt32 RDevMajor;
  UInt32 RDevMinor;
  UInt32 ChkSum;

  UInt32 Align;
  EType Type;

  UInt32 HeaderSize;
  UInt64 HeaderPos;

  bool IsBin() const { return Type == k_Type_BinLe || Type == k_Type_BinBe; }
  bool IsCrcFormat() const { return Type == k_Type_HexCrc; }
  bool IsDir() const { return MY_LIN_S_ISDIR(Mode); }
  bool IsTrailer() const { return strcmp(Name, kName_TRAILER) == 0; }
  UInt64 GetDataPosition() const { return HeaderPos + HeaderSize; }
};

enum EErrorType
{
  k_ErrorType_OK,
  k_ErrorType_Corrupted,
  k_ErrorType_UnexpectedEnd,
};

struct CInArchive
{
  ISequentialInStream *Stream;
  UInt64 Processed;
  
  HRESULT Read(void *data, size_t *size);
  HRESULT GetNextItem(CItem &item, EErrorType &errorType);
};

HRESULT CInArchive::Read(void *data, size_t *size)
{
  HRESULT res = ReadStream(Stream, data, size);
  Processed += *size;
  return res;
}

static bool ReadHex(const Byte *p, UInt32 &resVal)
{
  char sz[16];
  memcpy(sz, p, 8);
  sz[8] = 0;
  const char *end;
  resVal = ConvertHexStringToUInt32(sz, &end);
  return (unsigned)(end - sz) == 8;
}

static bool ReadOct6(const Byte *p, UInt32 &resVal)
{
  char sz[16];
  memcpy(sz, p, 6);
  sz[6] = 0;
  const char *end;
  resVal = ConvertOctStringToUInt32(sz, &end);
  return (unsigned)(end - sz) == 6;
}

static bool ReadOct11(const Byte *p, UInt64 &resVal)
{
  char sz[16];
  memcpy(sz, p, 11);
  sz[11] = 0;
  const char *end;
  resVal = ConvertOctStringToUInt64(sz, &end);
  return (unsigned)(end - sz) == 11;
}


#define READ_HEX(y) { if (!ReadHex(p2, y)) return S_OK; p2 += 8; }
#define READ_OCT_6(y) { if (!ReadOct6(p2, y)) return S_OK; p2 += 6; }
#define READ_OCT_11(y) { if (!ReadOct11(p2, y)) return S_OK; p2 += 11; }

static UInt32 GetAlignedSize(UInt32 size, UInt32 align)
{
  while ((size & (align - 1)) != 0)
    size++;
  return size;
}

static UInt16 Get16(const Byte *p, bool be) { if (be) return GetBe16(p); return GetUi16(p); }
static UInt32 Get32(const Byte *p, bool be) { return ((UInt32)Get16(p, be) << 16) + Get16(p + 2, be); }

#define G16(offs, v) v = Get16(p + (offs), be)
#define G32(offs, v) v = Get32(p + (offs), be)

static const unsigned kNameSizeMax = 1 << 12;

API_FUNC_static_IsArc IsArc_Cpio(const Byte *p, size_t size)
{
  if (size < k_BinRecord_Size)
    return k_IsArc_Res_NEED_MORE;

  UInt32 nameSize;
  UInt32 numLinks;
  if (p[0] == '0')
  {
    if (p[1] != '7' ||
        p[2] != '0' ||
        p[3] != '7' ||
        p[4] != '0')
      return k_IsArc_Res_NO;
    if (p[5] == '7')
    {
      if (size < k_OctRecord_Size)
        return k_IsArc_Res_NEED_MORE;
      for (int i = 6; i < k_OctRecord_Size; i++)
      {
        char c = p[i];
        if (c < '0' || c > '7')
          return k_IsArc_Res_NO;
      }
      ReadOct6(p + 6 * 6, numLinks);
      ReadOct6(p + 8 * 6 + 11, nameSize);
    }
    else if (p[5] == '1' || p[5] == '2')
    {
      if (size < k_HexRecord_Size)
        return k_IsArc_Res_NEED_MORE;
      for (int i = 6; i < k_HexRecord_Size; i++)
      {
        char c = p[i];
        if ((c < '0' || c > '9') &&
            (c < 'A' || c > 'F') &&
            (c < 'a' || c > 'f'))
          return k_IsArc_Res_NO;
      }
      ReadHex(p + 6 +  4 * 8, numLinks);
      ReadHex(p + 6 + 11 * 8, nameSize);
    }
    else
      return k_IsArc_Res_NO;
  }
  else
  {
    UInt32 rDevMinor;
    if (p[0] == kMagicBin0 && p[1] == kMagicBin1)
    {
      numLinks = GetUi16(p + 12);
      rDevMinor = GetUi16(p + 14);
      nameSize = GetUi16(p + 20);
    }
    else if (p[0] == kMagicBin1 && p[1] == kMagicBin0)
    {
      numLinks = GetBe16(p + 12);
      rDevMinor = GetBe16(p + 14);
      nameSize = GetBe16(p + 20);
    }
    else
      return k_IsArc_Res_NO;

    if (rDevMinor != 0)
      return k_IsArc_Res_NO;
    if (nameSize > (1 << 8))
      return k_IsArc_Res_NO;
  }
  if (numLinks == 0 || numLinks >= (1 << 10))
    return k_IsArc_Res_NO;
  if (nameSize == 0 || nameSize > kNameSizeMax)
    return k_IsArc_Res_NO;
  return k_IsArc_Res_YES;
}
}

#define READ_STREAM(_dest_, _size_) \
  { size_t processed = (_size_); RINOK(Read(_dest_, &processed)); \
if (processed != (_size_)) { errorType = k_ErrorType_UnexpectedEnd; return S_OK; } }

HRESULT CInArchive::GetNextItem(CItem &item, EErrorType &errorType)
{
  errorType = k_ErrorType_Corrupted;

  Byte p[k_RecordSize_Max];

  READ_STREAM(p, k_BinRecord_Size)

  UInt32 nameSize;

  if (p[0] != '0')
  {
    bool be;
         if (p[0] == kMagicBin0 && p[1] == kMagicBin1) { be = false; item.Type = k_Type_BinLe; }
    else if (p[0] == kMagicBin1 && p[1] == kMagicBin0) { be = true;  item.Type = k_Type_BinBe; }
    else return S_FALSE;

    item.Align = 2;
    item.DevMajor = 0;
    item.RDevMajor =0;
    item.ChkSum = 0;

    G16(2, item.DevMinor);
    G16(4, item.inode);
    G16(6, item.Mode);
    G16(8, item.UID);
    G16(10, item.GID);
    G16(12, item.NumLinks);
    G16(14, item.RDevMinor);
    G32(16, item.MTime);
    G16(20, nameSize);
    G32(22, item.Size);

    /*
    if (item.RDevMinor != 0)
      return S_FALSE;
    */

    item.HeaderSize = GetAlignedSize(nameSize + k_BinRecord_Size, item.Align);
    nameSize = item.HeaderSize - k_BinRecord_Size;
  }
  else
  {
    if (p[1] != '7' ||
        p[2] != '0' ||
        p[3] != '7' ||
        p[4] != '0')
      return S_FALSE;
    if (p[5] == kMagicOct)
    {
      item.Type = k_Type_Oct;
      READ_STREAM(p + k_BinRecord_Size, k_OctRecord_Size - k_BinRecord_Size)
      item.Align = 1;
      item.DevMajor = 0;
      item.RDevMajor = 0;

      const Byte *p2 = p + 6;
      READ_OCT_6(item.DevMinor);
      READ_OCT_6(item.inode);
      READ_OCT_6(item.Mode);
      READ_OCT_6(item.UID);
      READ_OCT_6(item.GID);
      READ_OCT_6(item.NumLinks);
      READ_OCT_6(item.RDevMinor);
      {
        UInt64 mTime64;
        READ_OCT_11(mTime64);
        item.MTime = 0;
        if (mTime64 < (UInt32)(Int32)-1)
          item.MTime = (UInt32)mTime64;
      }
      READ_OCT_6(nameSize);
      READ_OCT_11(item.Size);  // ?????
      item.HeaderSize = GetAlignedSize(nameSize + k_OctRecord_Size, item.Align);
      nameSize = item.HeaderSize - k_OctRecord_Size;
    }
    else
    {
      if (p[5] == kMagicHex)
        item.Type = k_Type_Hex;
      else if (p[5] == kMagicHexCrc)
        item.Type = k_Type_HexCrc;
      else
        return S_FALSE;

      READ_STREAM(p + k_BinRecord_Size, k_HexRecord_Size - k_BinRecord_Size)

      item.Align = 4;

      const Byte *p2 = p + 6;
      READ_HEX(item.inode);
      READ_HEX(item.Mode);
      READ_HEX(item.UID);
      READ_HEX(item.GID);
      READ_HEX(item.NumLinks);
      READ_HEX(item.MTime);
      {
        UInt32 size32;
        READ_HEX(size32);
        item.Size = size32;
      }
      READ_HEX(item.DevMajor);
      READ_HEX(item.DevMinor);
      READ_HEX(item.RDevMajor);
      READ_HEX(item.RDevMinor);
      READ_HEX(nameSize);
      READ_HEX(item.ChkSum);
      if (nameSize >= kNameSizeMax)
        return S_OK;
      item.HeaderSize = GetAlignedSize(nameSize + k_HexRecord_Size, item.Align);
      nameSize = item.HeaderSize - k_HexRecord_Size;
    }
  }
  if (nameSize > kNameSizeMax)
    return S_FALSE;
  if (nameSize == 0 || nameSize >= kNameSizeMax)
    return S_OK;
  char *s = item.Name.GetBuf(nameSize);
  size_t processedSize = nameSize;
  RINOK(Read(s, &processedSize));
  item.Name.ReleaseBuf_CalcLen(nameSize);
  if (processedSize != nameSize)
  {
    errorType = k_ErrorType_UnexpectedEnd;
    return S_OK;
  }
  errorType = k_ErrorType_OK;
  return S_OK;
}

class CHandler:
  public IInArchive,
  public IInArchiveGetStream,
  public CMyUnknownImp
{
  CObjectVector<CItem> _items;
  CMyComPtr<IInStream> _stream;
  UInt64 _phySize;
  EType _Type;
  EErrorType _error;
  bool _isArc;
public:
  MY_UNKNOWN_IMP2(IInArchive, IInArchiveGetStream)
  INTERFACE_IInArchive(;)
  STDMETHOD(GetStream)(UInt32 index, ISequentialInStream **stream);
};

static const Byte kArcProps[] =
{
  kpidSubType
};

static const Byte kProps[] =
{
  kpidPath,
  kpidIsDir,
  kpidSize,
  kpidMTime,
  kpidPosixAttrib,
  kpidLinks
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps

STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  switch (propID)
  {
    case kpidSubType: prop = k_Types[(unsigned)_Type]; break;
    case kpidPhySize: prop = _phySize; break;
    case kpidErrorFlags:
    {
      UInt32 v = 0;
      if (!_isArc)
        v |= kpv_ErrorFlags_IsNotArc;
      switch (_error)
      {
        case k_ErrorType_UnexpectedEnd: v |= kpv_ErrorFlags_UnexpectedEnd; break;
        case k_ErrorType_Corrupted: v |= kpv_ErrorFlags_HeadersError; break;
      }
      prop = v;
      break;
    }
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}


STDMETHODIMP CHandler::Open(IInStream *stream, const UInt64 *, IArchiveOpenCallback *callback)
{
  COM_TRY_BEGIN
  {
    Close();
    
    UInt64 endPos = 0;

    RINOK(stream->Seek(0, STREAM_SEEK_END, &endPos));
    RINOK(stream->Seek(0, STREAM_SEEK_SET, NULL));
    if (callback)
    {
      RINOK(callback->SetTotal(NULL, &endPos));
    }

    _items.Clear();
    CInArchive arc;

    arc.Stream = stream;
    arc.Processed = 0;

    for (;;)
    {
      CItem item;
      item.HeaderPos = arc.Processed;
      HRESULT result = arc.GetNextItem(item, _error);
      if (result == S_FALSE)
        return S_FALSE;
      if (result != S_OK)
        return S_FALSE;
      if (_error != k_ErrorType_OK)
      {
        if (_error == k_ErrorType_Corrupted)
          arc.Processed = item.HeaderPos;
        break;
      }
      if (_items.IsEmpty())
        _Type = item.Type;
      else if (_items.Back().Type != item.Type)
      {
        _error = k_ErrorType_Corrupted;
        arc.Processed = item.HeaderPos;
        break;
      }
      if (item.IsTrailer())
        break;
      
      _items.Add(item);

      {
        // archive.SkipDataRecords(item.Size, item.Align);
        UInt64 dataSize = item.Size;
        UInt32 align = item.Align;
        while ((dataSize & (align - 1)) != 0)
          dataSize++;

        // _error = k_ErrorType_UnexpectedEnd; break;

        arc.Processed += dataSize;
        if (arc.Processed > endPos)
        {
          _error = k_ErrorType_UnexpectedEnd;
          break;
        }

        UInt64 newPostion;
        RINOK(stream->Seek(dataSize, STREAM_SEEK_CUR, &newPostion));
        if (arc.Processed != newPostion)
          return E_FAIL;
      }

      if (callback && (_items.Size() & 0xFF) == 0)
      {
        UInt64 numFiles = _items.Size();
        RINOK(callback->SetCompleted(&numFiles, &item.HeaderPos));
      }
    }
    _phySize = arc.Processed;
    if (_error != k_ErrorType_OK)
    {
      if (_items.Size() == 0)
        return S_FALSE;
      if (_items.Size() == 1 && _items[0].IsBin())
      {
        // probably it's false detected archive. So we return error
        return S_FALSE;
      }
    }
    else
    {
      // Read tailing zeros.
      // Most of cpio files use 512-bytes aligned zeros
      UInt64 pos = arc.Processed;
      const UInt32 kTailSize_MAX = 1 << 9;
      Byte buf[kTailSize_MAX];
      
      UInt32 rem = (kTailSize_MAX - (UInt32)pos) & (kTailSize_MAX - 1);
      if (rem != 0)
      {
        rem++; // we need to see that it's end of file
        size_t processed = rem;
        RINOK(ReadStream(stream, buf, &processed));
        if (processed < rem)
        {
          unsigned i;
          for (i = 0; i < processed && buf[i] == 0; i++);
          if (i == processed)
            _phySize += processed;
        }
      }
    }
    
    _isArc = true;
    _stream = stream;
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::Close()
{
  _items.Clear();
  _stream.Release();
  _phySize = 0;
  _Type = k_Type_BinLe;
  _isArc = false;
  _error = k_ErrorType_OK;
  return S_OK;
}

STDMETHODIMP CHandler::GetNumberOfItems(UInt32 *numItems)
{
  *numItems = _items.Size();
  return S_OK;
}

STDMETHODIMP CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  const CItem &item = _items[index];

  switch (propID)
  {
    case kpidPath:
    {
      UString res;
      bool needConvert = true;
      #ifdef _WIN32
      if (ConvertUTF8ToUnicode(item.Name, res))
        needConvert = false;
      #endif
      if (needConvert)
        res = MultiByteToUnicodeString(item.Name, CP_OEMCP);
      prop = NItemName::GetOSName(res);
      break;
    }
    case kpidIsDir: prop = item.IsDir(); break;
    case kpidSize:
    case kpidPackSize:
      prop = (UInt64)item.Size;
      break;
    case kpidMTime:
    {
      if (item.MTime != 0)
      {
        FILETIME utc;
        NTime::UnixTimeToFileTime(item.MTime, utc);
        prop = utc;
      }
      break;
    }
    case kpidPosixAttrib: prop = item.Mode; break;
    case kpidLinks: prop = item.NumLinks; break;
    /*
    case kpidinode:  prop = item.inode; break;
    case kpidiChkSum:  prop = item.ChkSum; break;
    */
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

class COutStreamWithSum:
  public ISequentialOutStream,
  public CMyUnknownImp
{
  CMyComPtr<ISequentialOutStream> _stream;
  UInt64 _size;
  UInt32 _crc;
  bool _calculate;
public:
  MY_UNKNOWN_IMP
  STDMETHOD(Write)(const void *data, UInt32 size, UInt32 *processedSize);
  void SetStream(ISequentialOutStream *stream) { _stream = stream; }
  void ReleaseStream() { _stream.Release(); }
  void Init(bool calculate = true)
  {
    _size = 0;
    _calculate = calculate;
    _crc = 0;
  }
  void EnableCalc(bool calculate) { _calculate = calculate; }
  void InitCRC() { _crc = 0; }
  UInt64 GetSize() const { return _size; }
  UInt32 GetCRC() const { return _crc; }
};

STDMETHODIMP COutStreamWithSum::Write(const void *data, UInt32 size, UInt32 *processedSize)
{
  HRESULT result = S_OK;
  if (_stream)
    result = _stream->Write(data, size, &size);
  if (_calculate)
  {
    UInt32 crc = 0;
    for (UInt32 i = 0; i < size; i++)
      crc += (UInt32)(((const Byte *)data)[i]);
    _crc += crc;
  }
  if (processedSize)
    *processedSize = size;
  return result;
}

STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems,
    Int32 testMode, IArchiveExtractCallback *extractCallback)
{
  COM_TRY_BEGIN
  bool allFilesMode = (numItems == (UInt32)(Int32)-1);
  if (allFilesMode)
    numItems = _items.Size();
  if (numItems == 0)
    return S_OK;
  UInt64 totalSize = 0;
  UInt32 i;
  for (i = 0; i < numItems; i++)
    totalSize += _items[allFilesMode ? i : indices[i]].Size;
  extractCallback->SetTotal(totalSize);

  UInt64 currentTotalSize = 0;
  
  NCompress::CCopyCoder *copyCoderSpec = new NCompress::CCopyCoder();
  CMyComPtr<ICompressCoder> copyCoder = copyCoderSpec;

  CLocalProgress *lps = new CLocalProgress;
  CMyComPtr<ICompressProgressInfo> progress = lps;
  lps->Init(extractCallback, false);

  CLimitedSequentialInStream *streamSpec = new CLimitedSequentialInStream;
  CMyComPtr<ISequentialInStream> inStream(streamSpec);
  streamSpec->SetStream(_stream);

  COutStreamWithSum *outStreamSumSpec = new COutStreamWithSum;
  CMyComPtr<ISequentialOutStream> outStreamSum(outStreamSumSpec);

  for (i = 0; i < numItems; i++)
  {
    lps->InSize = lps->OutSize = currentTotalSize;
    RINOK(lps->SetCur());
    CMyComPtr<ISequentialOutStream> outStream;
    Int32 askMode = testMode ?
        NExtract::NAskMode::kTest :
        NExtract::NAskMode::kExtract;
    Int32 index = allFilesMode ? i : indices[i];
    const CItem &item = _items[index];
    RINOK(extractCallback->GetStream(index, &outStream, askMode));
    currentTotalSize += item.Size;
    if (item.IsDir())
    {
      RINOK(extractCallback->PrepareOperation(askMode));
      RINOK(extractCallback->SetOperationResult(NExtract::NOperationResult::kOK));
      continue;
    }
    if (!testMode && !outStream)
      continue;
    outStreamSumSpec->Init(item.IsCrcFormat());
    outStreamSumSpec->SetStream(outStream);
    outStream.Release();

    RINOK(extractCallback->PrepareOperation(askMode));
    RINOK(_stream->Seek(item.GetDataPosition(), STREAM_SEEK_SET, NULL));
    streamSpec->Init(item.Size);
    RINOK(copyCoder->Code(inStream, outStreamSum, NULL, NULL, progress));
    outStreamSumSpec->ReleaseStream();
    Int32 res = NExtract::NOperationResult::kDataError;
    if (copyCoderSpec->TotalSize == item.Size)
    {
      res = NExtract::NOperationResult::kOK;
      if (item.IsCrcFormat() && item.ChkSum != outStreamSumSpec->GetCRC())
        res = NExtract::NOperationResult::kCRCError;
    }
    RINOK(extractCallback->SetOperationResult(res));
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::GetStream(UInt32 index, ISequentialInStream **stream)
{
  COM_TRY_BEGIN
  const CItem &item = _items[index];
  return CreateLimitedInStream(_stream, item.GetDataPosition(), item.Size, stream);
  COM_TRY_END
}

static const Byte k_Signature[] = {
    5, '0', '7', '0', '7', '0',
    2, kMagicBin0, kMagicBin1,
    2, kMagicBin1, kMagicBin0 };

REGISTER_ARC_I(
  "Cpio", "cpio", 0, 0xED,
  k_Signature,
  0,
  NArcInfoFlags::kMultiSignature,
  IsArc_Cpio)

}}
